* feat(core): relay unsolicited agent events between user turns
Add a background goroutine per interactive session that consumes agent
events produced between user-initiated turns (e.g. Claude Code background
task completions via run_in_background). Previously these events piled up
in the buffered channel and were unconditionally discarded by drainEvents()
when the next user message arrived.
Key changes:
- Add startUnsolicitedReader/stopUnsolicitedReader with context-based
lifecycle management and bounded handoff to foreground turns
- Replace unconditional drainEvents with conditional resync via
eventsNeedResync flag (default true, cleared on clean EventResult)
- Integrate unsolicited reader teardown into all cleanup paths:
cleanupInteractiveState, stopInteractiveSession, session recycle,
/compress, and idle reaper
- Track BeginTurn/EndTurn for unsolicited turns so idle reaper does
not kill workspaces with active background processing
- Make workspace pool idle timeout configurable via
workspace_idle_timeout_mins in project config (was hardcoded 15min)
- Export SetWorkspaceIdleTimeout and DefaultWorkspaceIdleTimeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(core): add unit tests for unsolicited events feature
Add 10 tests covering the unsolicited events implementation:
- TestUnsolicitedReader_RelaysEventResult: verifies EventResult relay
- TestUnsolicitedReader_StopsOnCancel: verifies clean goroutine shutdown
- TestUnsolicitedReader_SetsResyncOnChannelClose: abnormal exit path
- TestUnsolicitedReader_SetsResyncOnEventError: error event handling
- TestUnsolicitedReader_PermissionDeny: permission auto-deny when no user
- TestEventsNeedResync_DefaultTrue: constructor initialization
- TestEventsNeedResync_ClearedOnCleanResult: clean EventResult path
- TestCleanupInteractiveState_StopsUnsolicitedReader: cleanup teardown
- TestWorkspaceIdleTimeout_Configurable: timeout config API
- TestReapIdle_DisabledWhenZeroTimeout: zero timeout disables reaping
Tests use event-driven synchronization (channel waits with timeouts)
instead of fixed time.Sleep to avoid CI flakiness.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): stop unsolicited reader in drainOrphanedQueue to prevent race
drainOrphanedQueue calls drainPendingMessages which reads from the
Events() channel. Without stopping the unsolicited reader first, two
goroutines can concurrently read from the same channel — a data race.
Also restart the unsolicited reader after draining completes so
background events continue to be relayed.
Found during self-review concurrency audit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(integration): add end-to-end tests for unsolicited events
Add two integration tests covering the full flow:
TestIntegration_UnsolicitedEventsEndToEnd — verifies the happy path:
1. User sends msg → agent turn completes (EventResult)
2. Agent emits new events later (simulating run_in_background completion)
→ unsolicited reader relays them to platform
3. User sends second msg → conditional drain skipped (clean exit) →
new turn response delivered correctly
TestIntegration_StaleEventsDrainedAfterAbnormalExit — verifies the safety
net: after EventError sets eventsNeedResync=true, buffered leftover events
are drained on the next user message (not relayed as unsolicited, not
attributed to the new turn).
Uses a persistentEventsSession that keeps the Events() channel open across
turns (unlike FakeAgentSession which closes per-call). Synchronization is
event-driven via waitForMessage and waitForPromptCount — no fixed sleeps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): address PR #608 review comments
Addresses all five review comments from @chenhg5 on PR #608:
1. **[blocker]** Promote workspace_idle_timeout_mins to a global config.
Adds top-level Config.WorkspaceIdleTimeoutMins; per-project
ProjectConfig.WorkspaceIdleTimeoutMins retains precedence so existing
configs keep working. main.go picks project > global > default.
2. Close the concurrent-reader window around stopUnsolicitedReader.
After cancel + bounded 5s wait, the reader now double-checks ctx
after reading an event — if cancellation happened between the outer
select firing and the check, the event is dropped and resync is
requested, preventing the old reader from silently consuming an event
that belongs to the incoming foreground turn. The wait stays bounded
because getOrCreateInteractiveStateWith and stopInteractiveSession
call this while holding interactiveMu, and an unbounded wait there
would stall unrelated sessions.
3. Notify the user on auto-deny. When approveAll=false, the reader now
sends a localized MsgBackgroundAutoDenied message (EN/ZH/ZH-TW/JA/ES)
identifying the requested tool so a silently blocked background task
isn't invisible. RespondPermission is dispatched on a detached
goroutine so the slow adapter call cannot block reader shutdown.
4. Accumulate and log tool events. EventToolUse / EventToolResult now
record tool names and emit debug logs. If the channel closes mid-turn
with buffered context, a warning log captures the tools/text that
were lost — removes the misleading "accumulate silently" comments.
5. Document Session.AddHistory thread-safety. Session.AddHistory is
already internally locked; stopUnsolicitedReader's bounded wait
orders unsolicited-vs-foreground history writes. Added an explanatory
comment so the invariant is explicit.
Verified with go test -race ./... and go test -race -tags=integration
./tests/integration/. Reviewed end-to-end by Codex (gpt-5.3-codex, high
reasoning effort) — no regressions flagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(config): make workspace_idle_timeout_mins global-only
Addresses the remaining blocker on PR #608: the maintainer asked for
workspace_idle_timeout_mins to be a global config rather than per-project.
Changes:
- Remove ProjectConfig.WorkspaceIdleTimeoutMins as a supported per-project
knob; the canonical setting is now the existing top-level
Config.WorkspaceIdleTimeoutMins.
- Keep parsing the legacy per-project key (renamed to
WorkspaceIdleTimeoutMinsLegacy in code) so existing configs continue to
work. When the top-level field is unset, the legacy per-project value is
honored as a fallback and a deprecation warning is logged at startup.
This avoids silently breaking deployments that used `0` to disable
reaping or a custom timeout.
- Clarify the comment on the global field to state that per-project
configuration is intentionally not supported.
Also commits the unsolicited-reader test additions in core/engine_test.go
(437 lines covering reader relay, cancel/stop, channel close, error
handling, permission deny, resync flag defaults/clearing, cleanup, and
workspace idle timeout configurability) that landed alongside the prior
review-fix commit but were not previously committed.
Verified via `codex exec review --uncommitted` (gpt-5.3-codex). Build
clean, all relevant tests pass; the 3 known macOS-specific pre-existing
failures (TestProcessInteractiveEvents_AppendsReplyFooter*,
TestResolveLocalDirPath_AcceptsSubdir) are out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
macOS does not have /bin/true — it lives at /usr/bin/true. Linux has
both paths, so switching to /usr/bin/true fixes the doctor user-isolation
check on macOS without breaking Linux.
Closes#735
Co-authored-by: Claude <noreply@anthropic.com>
When a cron job has WorkDir set but multi-workspace mode is not enabled,
getOrCreateWorkspaceAgent() would dereference the nil workspacePool and
panic. Add a nil check that returns an error instead, allowing the cron
executor to fall back to the global agent gracefully.
Closes#748
Co-authored-by: Claude <noreply@anthropic.com>
* feat(platform/max): add MAX messenger bridge with bidirectional file and voice support
- Implement core.Platform plus InlineButtonSender, ImageSender, FileSender,
AudioSender, MessageUpdater, FormattingInstructionProvider,
ReplyContextReconstructor
- Two-step attachment upload via POST /uploads + multipart to CDN
- Route incoming audio (type=audio and audio/* file mime) to
core.Message.Audio so the speech pipeline transcribes it for the agent
- Outgoing voice replies via SendAudio for the TTS pipeline
- Retry on attachment.not.ready with exponential backoff
- Long poll /updates with reconnect backoff and allow_from filtering
- Register max in Makefile ALL_PLATFORMS and document in config.example.toml
- Tighten .gitignore: ignore config.*.toml (except example) and *.old
- 19 unit/integration tests with httptest mock, passes -race
Refs #741
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(platform/max): handle real MAX CDN response shapes and audio download
Issues discovered when testing end-to-end with the live MAX API:
1. Image CDN returns nested {"photos": {<id>: {"token": ...}}}, not a flat
{"token": ...}. Parse the right shape per kind.
2. Audio CDN returns XML "<retval>1</retval>" rather than JSON. The real
attachment token is delivered in the /uploads response itself for
video/audio, so we use urlInfo.Token as the source of truth and only
scan the CDN body opportunistically.
3. /audios/{token} endpoint does not exist in MAX (returns 404). Incoming
voice payloads already carry the download URL directly, mirroring
images/files. Use payload.url; fall back to resolveMediaURL only if
empty (for video where token resolution may still apply).
Also raise the speech HTTP client timeout from 60s to 5m — enough for the
first-request cold-start case when a local Whisper backend has to download
or JIT-load the model before responding.
Tests updated to emit the real CDN response shapes per attachment type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(platform/max): address review — dedup + idiomatic session key parse
- Add core.MessageDedup to Platform and check msg.Body.Mid / callback ID
in handleMessage and handleCallback. Guards against reprocessing on
poll-loop reconnect edge cases, matching the pattern used by feishu/
dingtalk/wecom platforms.
- Replace fmt.Sscanf in ReconstructReplyCtx with strings.CutPrefix +
strings.Cut — more idiomatic Go, simpler to read, and handles the
"max:" prefix and optional ":userID" suffix without a manual loop.
- Add TestHandleMessageDedupsByID regression test.
Addresses review feedback on #742.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the opencode process crashes (e.g. stale session ID causes
'Session not found'), readLoop sends a fallback EventResult before
the deferred cmd.Wait() can detect and emit an EventError. The
engine receives EventResult(Done:true) first, returns from the
event loop, and the subsequent EventError is never consumed.
The user sees an empty response with no error information.
Fix by checking stderrBuf immediately after the scanner loop exits,
before sending the fallback EventResult. If stderr contains errors
(including 'Session not found'), emit EventError instead.
cmd.Wait() remains in a defer for resource cleanup only.
Engine uses a pattern table (agentErrorHandlers) to match known
errors and show localized user-friendly messages with recovery hints.
Co-authored-by: vibecoder <16832299+vibecoder@user.noreply.gitee.com>
1. Restore /btw as alias for /ps — prevents breaking change for existing users
2. Suppress done_emoji when agent reply is NO_REPLY (silent turn)
3. Handle NO_REPLY on channelClosed path — prevents leaking silent content
4. Add /show command to help text for zh-TW, ja, es (was only in en, zh)
5. Add TestBtwAlias_ResolvesToPs test
Made-with: Cursor
BridgePlatform was missing SendImage and SendFile methods, causing
`cc-connect send --image/--file` to silently succeed without delivering
content through the bridge protocol.
Add SendImage/SendFile following the existing SendAudio pattern (check
adapter capability, base64-encode data, send via sendToAdapter), plus
compile-time interface checks and protocol documentation for the new
`image` and `file` message types.
Closes#708
Co-authored-by: Claude <noreply@anthropic.com>
Add MarkdownToSlackMrkdwn() converter that transforms standard Markdown
to Slack's mrkdwn format before sending messages:
- **bold** → *bold*
- ***bold italic*** → *_bold italic_*
- ~~strike~~ → ~strike~
- [text](url) → <url|text>
- # Heading → *Heading*
- Code blocks and inline code are preserved untouched.
Applied in both Reply() and Send() methods of the Slack platform.
Closes#667
Co-authored-by: Claude <noreply@anthropic.com>
When the agent does not expose model/reasoning-effort/usage (e.g. the
claudecode agent) and the work directory equals $HOME,
compactReplyFooterPath returns "~" as the only part of the footer.
appendReplyFooter then wraps it as "*~*", which renders as a bare "~"
on Feishu/Weixin (both strip the surrounding italic markers).
A workdir alone isn't a useful status signal anyway, so skip the
footer entirely unless at least one status part (model, reasoning
effort, context remaining, or usage) is present.
Regression test covers an agent that only exposes GetWorkDir() and
runs in the user's home directory.
Add `!` prefix as a shorthand for `/shell` (e.g., `!ls -la`), with the
same admin permission checks. Also add optional `--timeout <seconds>`
parameter at the beginning of shell commands to override the default
60s timeout (e.g., `/shell --timeout 300 npm install`).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* pr-no_reply: suppress NO_REPLY marker responses
Add Mode-aware NO_REPLY handling on top of PR 655 display.Mode enum.
Agent can return a response beginning with 'NO_REPLY' to silently
skip platform delivery while preserving history + context.
- isSilentReply / couldBeSilentPrefix helpers (regex + prefix test)
- EventText: hold streaming preview while segment could still resolve
to a bare NO_REPLY marker; release (flush) once disambiguated
- silentHold reset at each segment boundary (thinking/tool/user)
- EventResult: detect isSilent on cleaned response; gate context
indicator, reply footer, TTS, and final send; call sp.discard()
to remove preview + skip done reaction via needsDoneReaction gate
* pr-no_reply: document NO_REPLY in injected system prompt
Agents cannot use the silent-reply feature unless they know about it.
Add a 'Silent reply' section to AgentSystemPrompt() describing when and
how to return the bare NO_REPLY marker.
* pr-no_reply: strip trailing NO_REPLY from mixed reasoning+marker responses
Extends NO_REPLY handling to match OpenClaw's stripSilentToken semantics.
Motivation: agents running cron/analysis tasks often narrate their
reasoning before appending NO_REPLY, defeating the bare-only detector.
- stripTrailingSilent helper (regex (?:^|\s+|\*+)NO_REPLY\s*$)
- EventResult: after bare-match check, try trailing strip;
empty result -> fully silent, non-empty -> deliver stripped text
- System prompt clarified: agent may end reply with NO_REPLY line;
stripped reasoning still delivers as normal reply
* test(core): add table-driven tests for NO_REPLY helpers
Cover isSilentReply, stripTrailingSilent, and couldBeSilentPrefix with
table-driven cases locking the contract these helpers expose to the
streaming preview and final-turn paths. Mirrors the coverage approach
used by PR #684 for its simpler strip helper.
isSilentReply: exact bare-marker match is case-insensitive and tolerates
surrounding whitespace/newlines; mixed content or tokens sharing the
prefix are rejected.
stripTrailingSilent: strips a trailing NO_REPLY preceded by whitespace
or markdown asterisks; leaves unchanged when the marker is absent,
mid-string, or glued to a longer token.
couldBeSilentPrefix: true while the trimmed input is a case-insensitive
prefix of NO_REPLY (including empty/whitespace-only), false once it
diverges or grows longer than the marker.
---------
Co-authored-by: Cigarrr <cigarrr@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add Homebrew as install option (brew install cc-connect)
Made-with: Cursor
* feat(core): make message queue depth configurable (#688)
Replace hardcoded maxQueuedMessages=5 with a configurable value via
[queue] max_depth in config.toml. When the queue is full, send a new
MsgQueueFull message with the current depth instead of the misleading
MsgPreviousProcessing.
- Add QueueConfig struct in config with max_depth field
- Add Engine.SetMaxQueuedMessages() setter, default remains 5
- Add MsgQueueFull i18n key with all 5 languages
- Wire config in main.go
- Update existing overflow test for new behavior
Closes#688
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Integration tests using real codex.Agent and claudecode.Agent adapters
with JSONL fixture files on disk. Tests the full pipeline: agent reads
real session files → ListSessions → Engine applySessionFilter → command
output. Covers /list, /switch, /delete under both filter modes plus
dynamic runtime toggle, for both Codex and Claude Code agents.
Made-with: Cursor
Session filtering that hides external CLI sessions is now opt-in via
`filter_external_sessions = true` in project config. By default, /list,
/switch, /delete show all agent sessions regardless of origin.
Includes integration tests covering /list, /switch, /delete, card
rendering, and dynamic toggle under both modes, plus config parse tests.
Made-with: Cursor
Complete the session name mapping coverage:
- Add name mapping to relay start (CompareAndSet without name)
- Add name mapping to relay EventText handler
- These paths were the last remaining gaps from the audit
Add end-to-end tests for all three agent SessionID patterns:
- Pattern A (ACP-like): CurrentSessionID() non-empty at start
→ name mapped in startOrResumeSession
- Pattern B (claudecode/gemini/cursor-like): ID in early EventText
→ name mapped in EventText handler
- Pattern C (codex/qoder/kimi-like): ID only in EventResult
→ name mapped in EventResult handler
All three tests simulate the full /new → message → reply → verify flow.
Made-with: Cursor
Codex agent's CurrentSessionID() returns "" at startup; the thread ID
is only set after Send() triggers thread.started. The session ID reaches
the engine through EventResult (turn.completed), not EventText.
The previous fix only added name mapping in startOrResumeSession and
EventText handlers, but for codex:
- startOrResumeSession: CurrentSessionID()="" → no-op
- EventText: codex doesn't set SessionID on text events → no-op
- EventResult: was using SetAgentSessionID (unconditional) instead of
CompareAndSetAgentSessionID, and had no name mapping at all
Now all three EventResult handlers (interactive, relay, drain) use
CompareAndSet and map the pending session name on first ID binding.
Added TestSessionName_CodexLikeFlow that simulates real codex behavior:
empty CurrentSessionID at startup, ID only in EventResult.
Made-with: Cursor
When /new creates a named session, the name was stored in Session.Name
but never mapped to the agent session ID in SessionManager.sessionNames.
The name mapping only happened in the EventText handler via
CompareAndSetAgentSessionID, but that returned false because
startOrResumeSession had already set the same ID moments earlier.
Now startOrResumeSession also calls SetSessionName when it first binds
the agent session ID to a session that has a pending custom name.
Made-with: Cursor
Sessions loaded from pre-fix data had their AgentSessionIDs silently
cleared by /new and provider switches. The previous fix (PastAgentSessionIDs)
only prevented future data loss but didn't handle already-corrupted data.
Changes:
- Make legacyData unconditional in KnownAgentSessionIDs (no hasPastIDs
check that could re-enable filtering prematurely after /new)
- Persist legacyData in session snapshot via LegacyData + Version fields
- Detect partially migrated data (PastIDTracking=true but untracked
sessions exist) during load via snapshot version comparison
- Auto-clear legacyData in saveLocked only when ALL sessions have tracking
- Add end-to-end test reproducing exact user scenario: legacy data with
15 sessions → /list → /new → message → /list before reply → reply →
/list with session name verification
- Add unit test for partially migrated data detection
Made-with: Cursor
- Rewrite SaveActiveProvider, SaveAgentModel, SaveProviderModel,
SaveLanguage, SaveDisplayConfig, SaveTTSMode to use surgical
text-level TOML editing instead of full Unmarshal→Encode cycle.
This preserves comments, unknown fields, and formatting.
- Add patchProjectAgentOption, patchTopLevelField, patchSectionField
helpers for targeted config edits.
- Extend rawProjectSpan to track [projects.agent] and
[projects.agent.options] sections.
- Add resetAllSessions to Engine; call it from management API
provider activate endpoint so sessions reset properly.
- Update docs/feishu.md: add card.action.trigger to required event
subscriptions; add FAQ for card button errors.
- Add startup log when feishu interactive card mode is enabled.
- Add tests for comment/field preservation in save functions.
Made-with: Cursor
- Add /skills route with local skills browser and recommended presets
- Backend: ListSkills/SkillDirs/AgentTypeName on Engine, skill presets API
- Provider preset cards: pin register/add buttons to card bottom
- Fix layout footer overlap by removing min-h-0 from content wrapper
- Replace logo with styled text brand, remove temporary local JSON override
- Add i18n translations for skill-related strings (en/zh/zh-TW/ja/es)
Made-with: Cursor
- Fix provider switch not taking effect: clear agent session ID on switch
to force fresh session instead of resuming with stale config
- Fix ANTHROPIC_MODEL env leak from cc-switch: explicitly set model env
when provider has custom BaseURL
- Add "clear provider" option in Feishu card for unsetting active provider
- Add debug logging for provider env vars and session startup commands
(with secret redaction via RedactEnv/RedactArgs)
- Web UI: add agent type switcher in project settings with provider
compatibility filtering and restart-required flow
- Web UI: filter global providers by agent_types when linking to project
- Web UI: fix heartbeat tab showing "running" for all projects (now shows
"not configured" when heartbeat is not enabled)
- Web UI: fix page not refreshing after agent type change + restart
- Add comprehensive integration tests for both claudecode and codex
provider switching (env vars, session args, config writing, send/receive)
- Complete i18n for all 5 languages: replyFooter, injectSender,
thinkingMessages, toolMessages and their hints
Made-with: Cursor
Add a "Import from CC-Switch" button to the web provider management
page. The backend reads the cc-switch SQLite database directly using
modernc.org/sqlite (pure Go, no CGO/sqlite3 CLI dependency), and the
frontend shows a modal with checkboxes to select which providers to
import as global providers.
- Add GET/POST /api/v1/providers/cc-switch management API endpoints
- Replace sqlite3 CLI usage with database/sql + modernc.org/sqlite
- Add CCSwitchImportModal component with i18n support (5 languages)
Made-with: Cursor
Provider switching was using the top-level BaseURL/Model from global
providers instead of per-agent-type overrides (Endpoints/AgentModels/
AgentModelLists). This caused agents like Codex to hit the wrong API
endpoint (e.g. /api/responses instead of /api/v1/responses), resulting
in 404 errors.
Also:
- Extract ProviderConfig.ResolveForAgent() to deduplicate override logic
- Cascade-delete provider_refs when a global provider is removed
- Purge deleted providers from running engines at runtime
- Add per-agent-type config UI for global provider form (tabbed when ≥2 agents)
- Add i18n translations for new form fields
Made-with: Cursor
- Restructure provider-presets.json (v1→v2): replace flat endpoints/
agent_models/models/agents/codex_config fields with a unified
`agents` map keyed by agent type, each containing base_url, model,
models list, and optional codex_config
- Update ProviderPreset Go struct with PresetAgentConfig and helper
methods SupportsAgent()/AgentConfig()
- Add AgentModelLists to config.ProviderConfig for per-agent model
lists; ResolveProviderRefs now resolves models per agent type
- Filter global provider linking by agent type in IM /provider add
card and executeProviderLink to prevent incompatible associations
- Fix Codex 404 caused by shengsuanyun agent_types including "codex"
with wrong base_url; remove codex from its agent_types
- Update web frontend ProviderPreset type and preset display/add logic
to use the new agents map structure
Made-with: Cursor
Star badge is now driven by the `featured` field in provider-presets.json.
Currently no preset has featured=true, so no stars are shown.
Can be toggled on per-provider remotely without code changes.
Made-with: Cursor
- Fix display_name from 省算云 to 胜算云 in provider-presets.json
- Show invite/register link in API key prompt card for preset providers
- Add MsgProviderAddInviteHint i18n key with all 5 languages
Made-with: Cursor
- Add global [[providers]] config with project-level provider_refs
- Add provider management page in web admin with preset support
- Implement card-based /provider add flow: preset selection card
with sponsor presets, API key prompt, and manual "Other" option
- Add inline button support for Telegram-style platforms
- Add remote provider-presets.json with sponsor data
- Add REST API endpoints for global providers and provider refs
- Update project detail providers tab with unified list UI
Made-with: Cursor
Previously done_emoji only fired when stream preview had been updated
in-place at least once, which meant quick responses never triggered
the reaction. Remove the needsDoneReaction() gate so the platform's
AddDoneReaction is always called on EventResult.
Made-with: Cursor
Remove the standalone /agentsid command and fold its output (Agent SID)
into /status. Also add work_dir to the status output so users can see
the current working directory at a glance.
Changes:
- Remove /agentsid command registration, handler, card button, help
text, and all i18n entries (MsgAgentSIDWarning, MsgBuiltinCmdAgentSID)
- Add Agent SID line to /status (both plain-text and card variants),
shown only when a session is active
- Add Work Dir line to /status output (i18n: 5 languages)
- Add MsgStatusAgentSID i18n key
Made-with: Cursor
Previously, /switch overwrote the active session's AgentSessionID,
causing the old agent session to disappear from KnownAgentSessionIDs
and thus from /list output.
Add SwitchToAgentSession() which finds an existing internal session
for the target agent session ID or creates a new one, preserving all
previous session references.
Made-with: Cursor
When quiet mode is enabled (ThinkingMessages=false, ToolMessages=false),
EventThinking and EventToolUse events are suppressed but the stream
preview card is not frozen. This causes multiple Claude text outputs
to accumulate into a single preview card.
Fix: when thinking/tool events arrive in quiet mode and there is
accumulated text, freeze + detachPreview to preserve the current text
as a permanent message. When the preview is already degraded (after
the first freeze), send accumulated text directly as a regular message
instead of silently advancing segmentStart past it.
Co-authored-by: leoliang <leoliang@futunn.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tables were rendered as proportional-width plain text, causing columns
to be misaligned in Telegram clients. Wrap tables in <pre> with
per-column padding so they display correctly in monospace.
Related: openclaw/openclaw#36323
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
H1: Kimi readLoop — move cmd.Wait() before EventResult to prevent
out-of-order EventError after turn completion.
H2: Kimi tests — add skipUnlessKimiAvailable guard so CI without
kimi binary skips gracefully instead of failing.
H3: Claude shellJoinArgs — add comprehensive unit tests for shell
quoting (spaces, single quotes, backslashes, newlines).
M1: cli_path — document that paths with spaces are not supported.
M3: needsDoneReaction — add streaming preview tests covering
UpdateMessage trigger, discard reset, and disabled state.
M5: /agentsid — add security warning about session ID sensitivity
in the command response.
M7: i18n — localize reply_footer_remaining for ZH/ZH-TW/JA/ES;
add MsgAgentSIDWarning translations for all 5 languages.
Made-with: Cursor
* feat(cli): add `cc-connect agent-sid` command to query agent session ID
Reads the agent session ID (e.g. Claude Code / Codex / Gemini CLI session
ID used with --resume) from persisted session files. Auto-detects project
and session key from CC_PROJECT and CC_SESSION_KEY env vars, making it
usable directly inside agent subprocess sessions.
Handles all session file naming variants: plain, hashed, workspace (_ws_),
legacy paths, and .sessions.json. When multiple files contain the same key,
prefers the one with the newest UpdatedAt. Includes nil-safety, file modtime
fallback for zero-UpdatedAt entries, and proper error propagation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(core): add /agentsid chat command to show agent session ID
Registers /agentsid (alias: /agent-sid) as a builtin chat command so
users can query the agent session ID directly from Discord/Telegram/
Feishu etc. without asking the agent to run a Bash command.
- Added to builtinCommands registry, handleCommand switch, and help
card (session group)
- Added to text-based /help output for all 5 languages (EN/ZH/ZH-TW/JA/ES)
- Added i18n description key MsgBuiltinCmdAgentSID with translations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(lint): use strings.TrimPrefix instead of HasPrefix+slice
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the agent's response is delivered via in-place card update (Feishu
streaming preview), the user gets no push notification. This adds a
configurable done_emoji option (e.g. "Done") that adds a reaction after
the "OnIt" reaction is removed, so the user knows the agent finished.
Only activates in quiet mode when the preview card was updated in-place
at least once (via UpdateMessage). Non-quiet mode is unaffected as
intermediate messages already trigger push notifications. Single-round
responses that only use SendPreviewStart are also unaffected, as the
initial send already triggers a push notification.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(core): add optional reply footer metadata
* feat(config): enable reply footer by default
* feat(codex): use runtime footer metadata from app-server
* feat(codex): resolve exec footer metadata at runtime
* feat(core): style reply footer and prefer context left
* feat(codex): use real runtime context for reply footer
* feat(core): use english left footer text
* fix(codex): use current context tokens for footer left
* fix(codex): match cli context left calculation
* test(codex): adapt footer tests to session base url arg
H1: Prevent workspace path traversal via ../
Add resolveLocalDirPath() that validates resolved path stays under
baseDir, expands ~/... via os.UserHomeDir, rejects escaping paths.
M2: Sanitize sender_name in inject_sender header
Replace quotes with single quotes, strip newlines/CR to prevent
prompt injection via display names.
M3: Warn on unset env var placeholders in config
resolveEnvPlaceholders now logs slog.Warn when ${VAR} references
an environment variable that is not set.
M4: Validate WeCom api_base_url
Reject non-http(s) or malformed URLs at New() to prevent credential
exfiltration via misconfigured base URL.
M5: Quote systemd Environment= values
Wrap values in double quotes to handle spaces, #, and special chars
in proxy URLs and paths.
M7: Discord table detection now requires separator row
Only wrap pipe-delimited blocks that contain a markdown separator
row (| --- | --- |), reducing false positives on decorative pipes.
Made-with: Cursor