From 6676d461753c49658b966f18da439cd27b87c705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Thu, 24 Sep 2026 19:08:35 +0100 Subject: [PATCH] fix(agent): fallback to session/new when auggie session not found (#3911) --- .../agent/runtime/lifecycle/session.go | 33 ++++++++++----- .../lifecycle/session_load_failure_test.go | 5 +++ .../lifecycle/session_missing_rollout_test.go | 40 +++++++++++++++++-- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/apps/backend/internal/agent/runtime/lifecycle/session.go b/apps/backend/internal/agent/runtime/lifecycle/session.go index 60b5c10b96..1afd01ccca 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/session.go +++ b/apps/backend/internal/agent/runtime/lifecycle/session.go @@ -251,7 +251,7 @@ func (sm *SessionManager) createOrLoadSession( zap.Bool("method_not_found", isMethodNotFoundErr(err)), zap.Bool("capability_mismatch", hasCanonicalSessionLoadMessage(err, "agent does not support session loading (LoadSession capability is false)")), zap.Bool("session_unknown", isSessionUnknownErr(err)), - zap.Bool("provider_session_missing", isMissingProviderRolloutErr(err, existingSessionID))) + zap.Bool("provider_session_missing", isMissingProviderSessionErr(err, existingSessionID))) return sm.createNewSession(ctx, client, agentConfig, workspacePath, mcpServers) } return sm.createNewSession(ctx, client, agentConfig, workspacePath, mcpServers) @@ -1905,7 +1905,7 @@ func isSessionLoadFallbackErr(err error, expectedSessionID string) bool { return false } if isMethodNotFoundErr(err) || isSessionUnknownErr(err) || - isMissingProviderRolloutErr(err, expectedSessionID) { + isMissingProviderSessionErr(err, expectedSessionID) { return true } return hasCanonicalSessionLoadMessage(err, "Method not found") || @@ -1915,7 +1915,9 @@ func isSessionLoadFallbackErr(err error, expectedSessionID string) bool { const ( jsonRPCInternalError = -32603 + jsonRPCInvalidParams = -32602 missingProviderRolloutPrefix = "no rollout found for thread id " + missingProviderSessionPrefix = "Session not found: " ) type sessionLoadRequestError struct { @@ -1926,24 +1928,25 @@ type sessionLoadRequestError struct { } `json:"data"` } -// isMissingProviderRolloutErr recognizes Codex's explicit not-found response -// after its process-local rollout state disappeared. The session ID must match -// the one Kandev attempted to load; unrelated internal errors remain fatal. -func isMissingProviderRolloutErr(err error, expectedSessionID string) bool { +// isMissingProviderSessionErr recognizes Codex's explicit not-found response +// after its process-local rollout state disappeared and Auggie's session not +// found error. The session ID must match the one Kandev attempted to load; +// unrelated internal errors remain fatal. +func isMissingProviderSessionErr(err error, expectedSessionID string) bool { if err == nil || strings.TrimSpace(expectedSessionID) == "" { return false } var reqErr *acp.RequestError if errors.As(err, &reqErr) { encoded, marshalErr := json.Marshal(reqErr) - if marshalErr == nil && matchesMissingProviderRollout(encoded, expectedSessionID) { + if marshalErr == nil && matchesMissingProviderSession(encoded, expectedSessionID) { return true } } message := err.Error() for offset := strings.IndexByte(message, '{'); offset >= 0; { candidate := message[offset:] - if matchesMissingProviderRollout([]byte(candidate), expectedSessionID) { + if matchesMissingProviderSession([]byte(candidate), expectedSessionID) { return true } next := strings.IndexByte(candidate[1:], '{') @@ -1955,14 +1958,22 @@ func isMissingProviderRolloutErr(err error, expectedSessionID string) bool { return false } -func matchesMissingProviderRollout(encoded []byte, expectedSessionID string) bool { +func matchesMissingProviderSession(encoded []byte, expectedSessionID string) bool { var projected sessionLoadRequestError if err := json.Unmarshal(encoded, &projected); err != nil { return false } - return projected.Code == jsonRPCInternalError && + if projected.Code == jsonRPCInternalError && projected.Message == "Internal error" && - projected.Data.Details == missingProviderRolloutPrefix+expectedSessionID + projected.Data.Details == missingProviderRolloutPrefix+expectedSessionID { + return true + } + if projected.Code == jsonRPCInvalidParams && + projected.Message == "Invalid params" && + projected.Data.Details == missingProviderSessionPrefix+expectedSessionID { + return true + } + return false } func hasCanonicalSessionLoadMessage(err error, canonical string) bool { diff --git a/apps/backend/internal/agent/runtime/lifecycle/session_load_failure_test.go b/apps/backend/internal/agent/runtime/lifecycle/session_load_failure_test.go index 171576411c..689163d2bf 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/session_load_failure_test.go +++ b/apps/backend/internal/agent/runtime/lifecycle/session_load_failure_test.go @@ -120,6 +120,11 @@ func TestInitializeSession_LoadCompatibilityFailureCreatesReplacement(t *testing message: `load session failed: failed to load session: {"code":-32603,"message":"Internal error",` + `"data":{"details":"no rollout found for thread id saved-session"}}`, }, + { + name: "missing auggie session", + message: `load session failed: failed to load session: {"code":-32602,"message":"Invalid params",` + + `"data":{"details":"Session not found: saved-session"}}`, + }, } for _, tt := range tests { diff --git a/apps/backend/internal/agent/runtime/lifecycle/session_missing_rollout_test.go b/apps/backend/internal/agent/runtime/lifecycle/session_missing_rollout_test.go index 45fbadda36..c65df68199 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/session_missing_rollout_test.go +++ b/apps/backend/internal/agent/runtime/lifecycle/session_missing_rollout_test.go @@ -8,7 +8,7 @@ import ( "github.com/coder/acp-go-sdk" ) -func TestIsMissingProviderRolloutErrRequiresMatchingStructuredEvidence(t *testing.T) { +func TestIsMissingProviderSessionErrRequiresMatchingStructuredEvidence(t *testing.T) { tests := []struct { name string err error @@ -55,12 +55,46 @@ func TestIsMissingProviderRolloutErrRequiresMatchingStructuredEvidence(t *testin `{"code":-32603,"message":"Internal error","data":{"details":"no rollout found for thread id saved-session"}} trailing`, ), }, + { + name: "matching auggie session not found error", + err: fmt.Errorf("load failed: %w", &acp.RequestError{ + Code: -32602, + Message: "Invalid params", + Data: map[string]any{ + "details": "Session not found: saved-session", + }, + }), + want: true, + }, + { + name: "matching auggie session not found in json string", + err: errors.New( + `load session failed: failed to load session: {"code":-32602,"message":"Invalid params","data":{"details":"Session not found: saved-session"}}`, + ), + want: true, + }, + { + name: "auggie session not found for different session", + err: &acp.RequestError{ + Code: -32602, + Message: "Invalid params", + Data: map[string]any{"details": "Session not found: other-session"}, + }, + }, + { + name: "auggie session not found with case mismatch", + err: &acp.RequestError{ + Code: -32602, + Message: "Invalid params", + Data: map[string]any{"details": "Session not found: SAVED-SESSION"}, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if got := isMissingProviderRolloutErr(test.err, "saved-session"); got != test.want { - t.Fatalf("isMissingProviderRolloutErr() = %t, want %t", got, test.want) + if got := isMissingProviderSessionErr(test.err, "saved-session"); got != test.want { + t.Fatalf("isMissingProviderSessionErr() = %t, want %t", got, test.want) } }) }