mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-03 12:28:10 +08:00
* 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>