109 Commits
Author SHA1 Message Date
ahahahaandClaude Opus 4.7 de5ca264aa feat(core): relay unsolicited agent events between user turns (#608)
* 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>
2026-04-30 17:25:33 +08:00
Claude fc79307142 fix(core): restore /btw alias, gate NO_REPLY done reaction, fix i18n gaps
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
2026-04-24 07:09:48 +08:00
LyInfi e67915227b fix(core): suppress reply footer when only workdir is known (#701)
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.
2026-04-24 06:32:40 +08:00
ee81bb59e6 feat(core): suppress agent replies marked with NO_REPLY (#682)
* 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>
2026-04-24 06:25:32 +08:00
Zeren ChenandClaude d01a9ccbb2 fix(telegram): show Telegram-safe skill commands in /skills (#571)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 06:24:32 +08:00
cg33andClaude Opus 4.6 39c0f9610a feat(core): make message queue depth configurable (#690)
* 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>
2026-04-23 22:36:40 +08:00
Claude 7a3bad4815 feat(session): add filter_external_sessions config (default off)
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
2026-04-21 08:02:21 +08:00
Claude 668c9d3ba6 fix(core): add name mapping to relay paths + tests for all agent patterns
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
2026-04-20 23:06:04 +08:00
Claude 86ab722c3e fix(core): map session name in EventResult handler (codex compatibility)
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
2026-04-20 22:57:22 +08:00
Claude b8a46c42b9 fix(core): map session name during startOrResumeSession
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
2026-04-20 22:49:09 +08:00
Claude 4e61c8f53c fix(core): persistent legacy mode for session filtering
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
2026-04-20 22:45:36 +08:00
Jerry d4addfb13f fix(codex): preserve workspace app-server options (#612)
* fix(codex): preserve workspace app-server options

* fix(core): tighten workspace agent option snapshots
2026-04-16 14:44:52 +08:00
xxb e119d54b5d feat(codex): add runtime reply footer aligned with Codex CLI (#578)
* 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
2026-04-16 14:44:11 +08:00
q107580018 b8d75f73af fix(feishu): improve delete and /model card refresh flows (#645) 2026-04-16 14:43:53 +08:00
Zeren Chen 41e7470ef5 (#597)
* fix(telegram): omit skill menu commands on overflow

* fix(telegram): clamp bot command descriptions
2026-04-16 14:41:24 +08:00
Claude bd9dc515f4 fix: address high/medium priority issues from PR review (batch 3)
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
2026-04-16 09:21:18 +08:00
cg33 9178b77fef Merge pull request #610 from g1f9/feat/workspace-init-local-dir
feat: support local directory paths in /workspace init
2026-04-16 07:45:36 +08:00
cg33 cfef710750 Merge pull request #586 from unimountltd/feat/sender-name-injection
feat(core): include sender display name in inject_sender header
2026-04-16 07:12:24 +08:00
Claude 0d92d808a3 fix: address high/medium priority issues from PR review
H1: Prevent double-Close on concurrent cleanupInteractiveState calls
    by nil'ing state.agentSession under the lock before closing.

H2: Weixin StartTyping sends typingStatusStop on ctx.Done() and on
    repeat-failure exit, preventing stuck "typing" indicator.

M3: Feishu @mention resolution: downgrade PII log to Debug, HTML-escape
    display names in at tags, skip ambiguous (duplicate) names.

M4: Narrow segmentStart==0 condition in EventResult to only apply when
    tool messages are hidden (!e.display.ToolMessages).

M5: Log warning when cron workspace agent creation fails instead of
    silently falling back to global agent.

M7: Discord file download capped at 50 MiB via io.LimitReader.

M8: Weixin getConfig now checks ret/errcode fields and returns error
    on non-zero values.
Made-with: Cursor
2026-04-15 20:41:32 +08:00
chendaxin.tkandClaude Opus 4.6 0553192ad6 feat: support local directory paths in /workspace init
Previously `/workspace init` only accepted git URLs. Users with existing
local directories had no way to bind them to a channel without cloning.

Now `/workspace init <path>` accepts:
- Absolute paths: `/workspace init /data/code/my-project`
- Relative names: `/workspace init my-project` (resolved under base_dir)
- Home-relative: `/workspace init ~/code/my-project`

The conversational init flow also accepts directory paths — users can
send a directory path when prompted instead of only git URLs.

Adds looksLikeLocalDir() helper, MsgWsInitDirNotFound and
MsgWsInitInvalidTarget i18n keys with translations for all 5 languages,
and 4 new test cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:02:38 +08:00
Jim TangandClaude Opus 4.6 108ab64ccf feat(core): include sender display name in inject_sender header
When inject_sender is enabled, the agent now receives the sender's
human-readable display name alongside the opaque user ID. This lets
agents distinguish between users by name in shared channel sessions
(e.g. Slack channels where multiple people talk to the same bot).

Header format changes from:
  [cc-connect sender_id=U123 platform=slack chat_id=C789]
to:
  [cc-connect sender_id=U123 sender_name="Jim Tang" platform=slack chat_id=C789]

The name is quoted to handle spaces. When no display name is available
(e.g. platform didn't resolve it), the header falls back to the
existing format without sender_name.

Changes:
- Add userName param to buildSenderPrompt and thread it through all 3
  call sites (direct send, queued message drain, event-loop drain)
- Add userName field to queuedMessage so queued messages preserve the
  display name across turns
- Add tests for empty name fallback and names with spaces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:50:00 +01:00
Claude d21cee5eb7 fix: pre-release fixes for high/medium priority issues from PR review
High priority:
- PR #420: fix Telegram reply context lost by alias resolution —
  resolveAlias now runs on user text before ExtraContent merge
- PR #567: fix message queue race — adoptPendingFromPlaceholder
  copies queued messages when placeholder state is replaced
- PR #437: harden /diff command — reject targets starting with '-',
  use '--' separator to prevent git option injection
- PR #560: add run_as_env dangerous variable blacklist (LD_PRELOAD,
  PATH, HOME, etc.); fix doc claiming PATH is in default allowlist

Medium priority:
- PR #497: ACP permission params log level Info→Debug to prevent
  sensitive data leaking; add toolInputByID cache size cap (1000)
- PR #559: observe new JSONL files start from EOF instead of offset 0,
  preventing full session history replay to Slack
- PR #510: add idempotent Close() test for claude session
- PR #514: bridge ReconstructReplyCtx already has structured JSON +
  backward compat; added decode helper + tests
- PR #440: add instance lock acquire/release unit tests
- PR #569: document ContinueSession behavioral change in code comments

All changes backward-compatible. Tests added for all fixes.

Made-with: Cursor
2026-04-12 20:40:02 +08:00
cg33andClaude 780a81af42 fix(core): filter /list and /switch to only show cc-connect owned sessions (#569)
External Claude CLI sessions in the same work_dir were appearing in /list,
/switch, /delete, and /search results because ListSessions() scans all
.jsonl files in the project directory without filtering.

- Add KnownAgentSessionIDs() to SessionManager to collect tracked agent IDs
- Add filterOwnedSessions() to exclude external CLI sessions from all
  session listing operations (9 call sites in engine.go)
- Remove dead --continue code path from claudecode/session.go (engine no
  longer passes ContinueSession sentinel)
- Add regression tests for the filtering logic

Closes #541

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 06:19:26 +08:00
cg33andClaude 4bb14d47d0 fix(core): queue messages during session startup instead of dropping (#565) (#567)
When a session lock is held and a new message arrives, the queue
mechanism requires an interactiveState entry to exist. Previously,
this entry was only created inside the async goroutine after the
agent process was spawned — a potentially slow operation. Messages
arriving during this startup window were silently dropped.

Fix:
- Create a placeholder interactiveState before launching the async
  processor so the queue entry point exists immediately
- Relax queueMessageForBusySession guard to allow queueing when
  agentSession is nil (startup phase), while still rejecting when
  the agent session was established but has died

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 06:19:20 +08:00
af18bf8b31 fix(core): fix flaky shell test on macOS due to symlink path resolution (#561)
normalizeWorkspacePath resolves /var/folders/ to /private/var/folders/
on macOS, but shell pwd returns the original path. The test assertion now
checks both the original and resolved paths.

Co-authored-by: happytonakai <hanzr.nju@outlook.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 19:45:11 +08:00
8dd5868d72 feat(observe): mirror terminal Claude Code sessions to a chat channel (#559)
* feat(observe): add --observe CLI flag and engine config

Add observe fields to the Engine struct and a SetObserveConfig setter
to enable terminal session observation in a later task. Define --observe
and --observe-channel CLI flags (wiring deferred).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(observe): add ObserverTarget interface, implement for Slack

Define ObserverTarget optional interface in core/ for platforms that can
receive terminal observation messages. Implement SendObservation on Slack
platform using PostMessageContext with link unfurl disabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(observe): implement JSONL session file watcher and parser

Adds sessionObserver to core/observer.go which tails ~/.claude/projects/{projectKey}/*.jsonl
files, parses user/assistant events, filters out sdk-cli sessions (cc-connect's own),
and forwards new messages to ObserverTarget. Includes full test coverage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(observe): wire observer into engine startup and shutdown

Add findObserverTarget to locate the first ObserverTarget platform,
startObserver to launch the JSONL watcher goroutine when configured,
wired into Engine.Start() and cancelled in Engine.Stop().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(observe): wire --observe and --observe-channel flags in CLI

Removes the TODO suppressor and connects the --observe/--observe-channel
CLI flags to engine.SetObserveConfig(), resolving the Claude Code project
directory from the configured work_dir via the new resolveClaudeProjectDir helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(observe): add config.toml support for observe settings

Add ObserveConfig struct to ProjectConfig so users can configure terminal
session observation via [projects.observe] in config.toml, with CLI flags
(--observe, --observe-channel) taking precedence over config file values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(observe): rune-safe truncation, robust offset tracking, interface assertion

- Use file size for offset tracking instead of line-length calculation
  to avoid drift from \r\n line endings or partial final lines
- Truncate messages at valid rune boundaries to prevent garbled UTF-8
- Add compile-time ObserverTarget interface assertion for Slack

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Leigh Stillard <leigh@stillard.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 19:45:05 +08:00
ConverSun bb37af823a fix: persist multi-workspace /dir overrides without cross-channel leakage (#505)
* fix: persist per-workspace dir overrides across agent recreation

In multi-workspace mode, /dir overrides were lost after daemon restart
or workspace pool eviction because getOrCreateWorkspaceAgent always
recreated agents with work_dir=workspace root, ignoring the override
stored in project state.

Changes:
- Add WorkspaceDirOverrides map to ProjectStateStore for per-workspace
  dir override persistence
- Apply persisted override when recreating workspace agents in
  getOrCreateWorkspaceAgent
- Store workspace-specific overrides in dirApply when in multi-workspace
  mode (set and reset)
- Auto-clear invalid overrides (missing/non-directory paths)

Backward compatible: existing work_dir_override field preserved for
single-workspace mode.

* fix: use per-channel dir overrides instead of per-workspace

Two channels bound to the same workspace were sharing /dir overrides.
Change the override key from workspace path to interactiveKey
(workspace:sessionKey) so each channel maintains independent dir state.

- Remove override from getOrCreateWorkspaceAgent (shared agent layer)
- Add applyChannelDirOverride helper, called in handleMessage and
  commandContext (per-channel routing layer)
- dirApply now stores/clears overrides keyed by interactiveKey

* refactor: centralize multi-workspace context resolution

Use a shared workspaceContext helper so commandContext and message
routing build the same workspace agent/session/interactiveKey state in
one place. This keeps the per-channel dir override fix unchanged while
removing duplicate override application logic.

* fix: route multi-workspace dir overrides by effective work dir

Avoid mutating shared workspace agents when applying per-channel /dir
overrides. Resolve the effective work dir from project state and route
each channel to an agent context keyed by that dir instead.

This removes cross-channel work_dir races while preserving existing
/dir persistence and reset behavior.
2026-04-09 18:55:30 +08:00
Jerry bfb6d69780 Feature: normalize local references for IM rendering and add /show for direct file inspection (#495)
* feat(references): normalize and render local agent references

* feat(show): add reference-aware file and directory viewing

* docs: document references config and show usage

* chore: remove local working docs from PR branch

* fix: address lint issues in reference pipeline

* fix: drop stale quiet leftovers after main rebase

* fix: clean remaining rebase markers in show tests
2026-04-09 16:50:25 +08:00
Claude f70b98f6d5 fix(core): restore /quiet command removed by PR #462
The /quiet slash command was a frequently used user-facing feature
that was accidentally removed when thinking/tool visibility was
split into separate config items. Restore it as a toggle that
flips both ThinkingMessages and ToolMessages together, persisting
the change via displaySaveFunc. Also restores /quiet in the help
card listing.

Made-with: Cursor
2026-04-09 15:21:53 +08:00
cg33 1c0c6ce5b4 Merge pull request #506 from q107580018/opencode-model-cache
feat(opencode): significantly speed up /model switching
2026-04-09 14:30:37 +08:00
Mr.QiuW 1641c42269 feat(opencode): significantly speed up model switching 2026-04-09 01:11:19 +08:00
naidongzZZandClaude Opus 4.6 2048b358cf feat(core): add /diff command to generate HTML diff via diff2html (#437)
Add admin-only /diff [target] command that runs git diff, pipes output
through diff2html-cli to produce an HTML file, and sends it via
FileSender. Falls back to plain text diff with install hint when
diff2html is not available or platform lacks file sending support.

Filename uses branch-commitID.html, HTML title shows branch vs target.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:31:03 +08:00
Claude d1d93f60b0 fix: address high/medium priority issues from PR review
- PR #445: auto-compress skips when agent has no CompressCommand;
  --max-context-tokens is now configurable via agent options instead
  of being hardcoded to 900000
- PR #448: codex app-server RPC timeout cleans pending map entry;
  emit() uses non-blocking send to prevent readLoop stall;
  readLoop uses scanner with 10MB max line size
- PR #462: restore backward-compatible quiet config — maps to
  display.thinking_messages/tool_messages when [display] not set
- PR #479: Gemini STT sends API key via x-goog-api-key header
  instead of URL query param; model name is URL-escaped
- PR #489: feishu fetchQuotedMessage uses caller context instead
  of context.Background; extractPostPlainText includes code_block
- PR #447/#474: SendToSessionWithAttachments uses workspace-prefix
  stripping fallback matching cron/heartbeat logic
- PR #446: PATH prepend for custom commands limited to Windows only

All changes are backward-compatible; new behavior gated by config.
Tests added for quiet config, workspace prefix, Gemini header, and
feishu code_block extraction.

Made-with: Cursor
2026-04-08 23:11:39 +08:00
Li Yan a0a7cc50f7 feat: split thinking/tool visibility and preserve QQ Bot references (#462) 2026-04-08 22:26:48 +08:00
Jerry 33551b9a7e fix(core): avoid reaping active multi-workspace sessions (#463) 2026-04-08 22:26:44 +08:00
Leigh StillardandClaude Sonnet 4.6 326fa258ff fix(core): strip workspace prefix from cron/heartbeat session keys (#474)
In multi-workspace mode, stored session keys may be prefixed with the
workspace path (e.g. "/home/user/project:slack:C123:U456"). The naive
first-colon split produced a path as the platform name, causing
ExecuteCronJob and ExecuteHeartbeat to fail with "platform not found".

Add a fallback loop that searches for a registered platform name within
the key and strips the prefix before proceeding.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:26:31 +08:00
98ef87ce92 fix(core): expand tilde (~) in /dir command path (#366)
* fix(core): expand tilde in /dir command path

Support ~ and ~/ prefix in /dir command so users can type paths like
~/workspace/project instead of requiring full absolute paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): add test coverage for tilde expansion in /dir command

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: zhongshenghua <15013051024@126.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 09:35:33 +08:00
Jerry b081eb1303 feat(display): add tool progress message toggle (#361) 2026-04-02 09:34:53 +08:00
Jerry d76639360c feat(core): rotate sessions after idle inactivity (#400) 2026-04-02 09:26:35 +08:00
Claude e0e2be4e31 fix(core): 出站限流补全、工具结果展示与 i18n 收紧
- waitOutgoing/sendWithError:重启通知、SendToSession(含图片/文件)、权限与 AskQuestion 内联按钮路径纳入出站令牌桶

- EventResult 分块与 channelClosed 收尾使用 sendWithError,首包失败即停止发送

- ProgressStyleProvider 且 progress_style=legacy 时不再单独推送 EventToolResult 聊天消息(compact/card 仍走 writer)

- 工具结果 fallback 标签改为 MsgKey,补全日/西/繁中翻译

- config.example.toml:说明小数 max_per_second 时 burst 取 ceil

- 修复 processInteractiveEvents 测试参数;新增 SuppressStandaloneToolResultEvent 单测

- fix(codex): 假 codex 脚本用 stdin 读 prompt,适配 --json -
2026-03-28 00:42:35 +08:00
Jerry b56865fb06 feat(feishu): add configurable progress styles (compact/card) to reduce message spam
* feat(feishu): add compact progress style for thinking/tool updates

* feat(feishu): add card-style streaming progress updates
2026-03-28 00:16:13 +08:00
Deeka WongandDeeka Wong 1225b7ee18 feat(claudecode): add auto permission mode
Co-authored-by: Deeka Wong <8337659+huangdijia@users.noreply.github.com>
2026-03-27 23:42:53 +08:00
Jerry 5639e863d5 fix(core): route workspace model changes through session context 2026-03-27 23:31:08 +08:00
xxb 526f52e5fc fix(codex): force kill session process group on stop (#340)
* fix(codex): force kill session process group on stop

* fix(codex): kill all active session processes on stop

* fix(core): stop stale event loops on stop
2026-03-27 23:11:16 +08:00
Jerry b04ece2dd7 fix(core): normalize interactive session key lookup in multi-workspace mode (#267)
* fix(core): use interactive key for status card and SendToSession lookups in multi-workspace

* fix(core): remove unused new session assignment
2026-03-27 09:36:31 +08:00
xxb 6d8bf93e4c fix: harden Discord session routing and remove implicit continue bridge (#322)
* fix(discord): dedup duplicate interaction events

* fix(core): remove implicit continue bridge

* fix(discord): avoid empty button reply references
2026-03-25 14:31:27 +08:00
xxb a44b375bce fix(core): exact-resume saved session on workspace reconnect (#315) 2026-03-25 06:30:30 +08:00
Claude 2700b5b027 Merge remote-tracking branch 'github/main' (sync merged PRs) 2026-03-24 14:19:13 +08:00
cg33andClaude dc8808ea1a fix(engine): /new must not reuse prior Claude process (fixes #238) (#297)
* fix(codex): avoid closing events channel before readLoop exits

Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.

Fixes https://github.com/chenhg5/cc-connect/issues/281

* fix(engine): recycle live agent when Session clears AgentSessionID (/new)

getOrCreateInteractiveStateWith reused any alive process when wantID was empty
because the condition treated empty wantID as always compatible. After /new the
Session has no resume ID but the old Claude process still reported the previous
session — messages kept going to stale context.

Recycle when the live process has a concrete ID but the Session expects none or
a different one.

Fixes #238

Made-with: Cursor

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-24 14:16:23 +08:00
Claude ac79163082 fix(engine): pass replyCtx as parameter to processInteractiveEvents
Avoids race condition when cron and regular messages share the same
interactiveKey — the shared state.replyCtx was being overwritten by
concurrent goroutines, causing cron responses to be delivered to the
wrong session.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 04:14:51 +00:00