mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-07 23:33:38 +08:00
feat: fork session on --continue to prevent context contamination (#244)
When cc-connect starts up and picks up the most recent CLI session via --continue, it now adds --fork-session to create an independent context branch. This prevents history interleaving when the user is concurrently working in the same workspace from both the CLI and a messaging platform. Without this, resuming into an active CLI session corrupts both context windows — the model sees interleaved tool calls and responses from two different conversations, leading to hallucinations about files and state. The fork only applies to --continue (first connection after startup, where we grab "whatever is latest" which may be an active CLI session). Plain --resume of cc-connect's own stored session ID is unaffected, since that session belongs to cc-connect and has no interleaving risk. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,17 @@ func newClaudeSession(ctx context.Context, workDir, model, sessionID, mode strin
|
||||
if mode != "" && mode != "default" {
|
||||
args = append(args, "--permission-mode", mode)
|
||||
}
|
||||
if sessionID != "" {
|
||||
switch sessionID {
|
||||
case "":
|
||||
// Truly fresh session — no resume, no continue.
|
||||
case core.ContinueSession:
|
||||
// --continue grabs the most recent session in the workspace, which
|
||||
// may belong to an active CLI terminal. Fork it so the platform
|
||||
// conversation gets its own independent context branch.
|
||||
args = append(args, "--continue", "--fork-session")
|
||||
default:
|
||||
// Resuming a known session ID — this is cc-connect's own session
|
||||
// from a previous connection, safe to resume directly.
|
||||
args = append(args, "--resume", sessionID)
|
||||
}
|
||||
if model != "" {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
@@ -192,6 +193,8 @@ type Engine struct {
|
||||
|
||||
quietMu sync.RWMutex
|
||||
quiet bool // when true, suppress thinking and tool progress messages globally
|
||||
|
||||
hasConnectedOnce atomic.Bool // first connection uses --continue to bridge CLI usage
|
||||
}
|
||||
|
||||
// workspaceInitFlow tracks a channel that is being onboarded to a workspace.
|
||||
@@ -1739,9 +1742,17 @@ func (e *Engine) getOrCreateInteractiveStateWith(sessionKey string, p Platform,
|
||||
return state
|
||||
}
|
||||
|
||||
agentSID := session.GetAgentSessionID()
|
||||
// On first connection after engine startup, use --continue to pick up
|
||||
// the most recent CLI session (bridges direct CLI and cc-connect usage).
|
||||
// Subsequent connections respect the stored session ID (or "" for fresh).
|
||||
startSessionID := session.GetAgentSessionID()
|
||||
if !e.hasConnectedOnce.Swap(true) {
|
||||
startSessionID = ContinueSession
|
||||
}
|
||||
|
||||
isResume := startSessionID != ""
|
||||
startAt := time.Now()
|
||||
agentSession, err := agent.StartSession(e.ctx, agentSID)
|
||||
agentSession, err := agent.StartSession(e.ctx, startSessionID)
|
||||
startElapsed := time.Since(startAt)
|
||||
if err != nil {
|
||||
slog.Error("failed to start interactive session", "error", err, "elapsed", startElapsed)
|
||||
@@ -1750,7 +1761,7 @@ func (e *Engine) getOrCreateInteractiveStateWith(sessionKey string, p Platform,
|
||||
return state
|
||||
}
|
||||
if startElapsed >= slowAgentStart {
|
||||
slog.Warn("slow agent session start", "elapsed", startElapsed, "agent", agent.Name(), "session_id", agentSID)
|
||||
slog.Warn("slow agent session start", "elapsed", startElapsed, "agent", agent.Name(), "session_id", startSessionID)
|
||||
}
|
||||
|
||||
if newID := agentSession.CurrentSessionID(); newID != "" {
|
||||
@@ -1767,7 +1778,7 @@ func (e *Engine) getOrCreateInteractiveStateWith(sessionKey string, p Platform,
|
||||
}
|
||||
e.interactiveStates[sessionKey] = state
|
||||
|
||||
slog.Info("interactive session started", "session_key", sessionKey, "agent_session", session.GetAgentSessionID(), "elapsed", startElapsed)
|
||||
slog.Info("session spawned", "session_key", sessionKey, "agent_session", session.GetAgentSessionID(), "is_resume", isResume, "elapsed", startElapsed)
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,11 @@ const (
|
||||
EventThinking EventType = "thinking" // thinking/processing status
|
||||
)
|
||||
|
||||
// ContinueSession is a sentinel value passed to Agent.StartSession to indicate
|
||||
// that the agent should pick up the most recent session in the workspace
|
||||
// (equivalent to `claude --continue`), rather than resuming a specific session ID.
|
||||
const ContinueSession = "__continue__"
|
||||
|
||||
// UserQuestion represents a structured question from AskUserQuestion.
|
||||
type UserQuestion struct {
|
||||
Question string `json:"question"`
|
||||
|
||||
Reference in New Issue
Block a user