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:
1 parent
6ad4d49950
commit
df5cc64ef4
3 files changed
+31
-5
No files matched your search
@@ -52,7 +52,17 @@ func newClaudeSession(ctx context.Context, workDir, model, sessionID, mode strin
|
|||||||
if mode != "" && mode != "default" {
|
if mode != "" && mode != "default" {
|
||||||
args = append(args, "--permission-mode", mode)
|
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)
|
args = append(args, "--resume", sessionID)
|
||||||
}
|
}
|
||||||
if model != "" {
|
if model != "" {
|
||||||
|
|||||||
+15
-4
@@ -15,6 +15,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
@@ -192,6 +193,8 @@ type Engine struct {
|
|||||||
|
|
||||||
quietMu sync.RWMutex
|
quietMu sync.RWMutex
|
||||||
quiet bool // when true, suppress thinking and tool progress messages globally
|
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.
|
// 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
|
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()
|
startAt := time.Now()
|
||||||
agentSession, err := agent.StartSession(e.ctx, agentSID)
|
agentSession, err := agent.StartSession(e.ctx, startSessionID)
|
||||||
startElapsed := time.Since(startAt)
|
startElapsed := time.Since(startAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to start interactive session", "error", err, "elapsed", startElapsed)
|
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
|
return state
|
||||||
}
|
}
|
||||||
if startElapsed >= slowAgentStart {
|
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 != "" {
|
if newID := agentSession.CurrentSessionID(); newID != "" {
|
||||||
@@ -1767,7 +1778,7 @@ func (e *Engine) getOrCreateInteractiveStateWith(sessionKey string, p Platform,
|
|||||||
}
|
}
|
||||||
e.interactiveStates[sessionKey] = state
|
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
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ const (
|
|||||||
EventThinking EventType = "thinking" // thinking/processing status
|
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.
|
// UserQuestion represents a structured question from AskUserQuestion.
|
||||||
type UserQuestion struct {
|
type UserQuestion struct {
|
||||||
Question string `json:"question"`
|
Question string `json:"question"`
|
||||||
|
|||||||
Reference in new issue
Block a user