fix(chat): preserve Gemini CLI history sessions on reopen
When reopening a Gemini CLI history session, session/load fails with "Authentication required" and the fallback session/new overwrites the DB external_id with a new session ID that has no corresponding file, causing all historical messages to disappear. - Skip session/new when session/load returns "Authentication required" - Add Gemini to the parser fallback so stale external_ids recover via folder_path + started_at matching - Guard externalIdSavedRef for existing conversations to prevent session/new from overwriting the persisted external_id - Only update conversation status on disconnect when user has sent a message, avoiding spurious "completed" flips on pure history views Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -844,6 +844,12 @@ async fn run_connection(
|
|||||||
// Only emit a visible error for unexpected failures;
|
// Only emit a visible error for unexpected failures;
|
||||||
// "Method not found" is expected for agents that don't
|
// "Method not found" is expected for agents that don't
|
||||||
// support session resume (e.g. Cline).
|
// support session resume (e.g. Cline).
|
||||||
|
// "Authentication required" is expected for agents whose
|
||||||
|
// credentials have expired (e.g. Gemini CLI) — skip
|
||||||
|
// session/new too since it will also fail.
|
||||||
|
if err_str.contains("Authentication required") {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if !err_str.contains("Method not found") {
|
if !err_str.contains("Method not found") {
|
||||||
crate::web::event_bridge::emit_event(
|
crate::web::event_bridge::emit_event(
|
||||||
&emitter_clone,
|
&emitter_clone,
|
||||||
|
|||||||
@@ -289,11 +289,12 @@ pub async fn get_folder_conversation_core(
|
|||||||
match parser.get_conversation(&eid) {
|
match parser.get_conversation(&eid) {
|
||||||
Ok(d) => Ok((d.turns, d.session_stats, None)),
|
Ok(d) => Ok((d.turns, d.session_stats, None)),
|
||||||
Err(crate::parsers::ParseError::ConversationNotFound(_)) => {
|
Err(crate::parsers::ParseError::ConversationNotFound(_)) => {
|
||||||
// For agents like OpenClaw and Cline, the external_id is an
|
// The external_id may no longer match any local file —
|
||||||
// ACP session UUID that doesn't correspond to any local file.
|
// e.g. an ACP session UUID (OpenClaw, Cline) or a stale
|
||||||
// Fall back to matching by folder_path and started_at from
|
// ID after session/new fallback overwrote the original
|
||||||
// the parsed conversation list.
|
// (Gemini CLI). Fall back to matching by folder_path
|
||||||
if at == AgentType::OpenClaw || at == AgentType::Cline {
|
// and started_at from the parsed conversation list.
|
||||||
|
if matches!(at, AgentType::OpenClaw | AgentType::Cline | AgentType::Gemini) {
|
||||||
if let Ok(all) = parser.list_conversations() {
|
if let Ok(all) = parser.list_conversations() {
|
||||||
// Filter by folder_path first, then find the closest
|
// Filter by folder_path first, then find the closest
|
||||||
// started_at match within 300 seconds of db_created_at.
|
// started_at match within 300 seconds of db_created_at.
|
||||||
|
|||||||
@@ -225,7 +225,9 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
// resolves, we can't update the DB status yet. This ref records the
|
// resolves, we can't update the DB status yet. This ref records the
|
||||||
// desired status so the createConversation callback can apply it.
|
// desired status so the createConversation callback can apply it.
|
||||||
const deferredStatusRef = useRef<string | null>(null)
|
const deferredStatusRef = useRef<string | null>(null)
|
||||||
const externalIdSavedRef = useRef(false)
|
// For existing conversations (opened from sidebar), the external_id is
|
||||||
|
// already persisted — don't let a session/new fallback overwrite it.
|
||||||
|
const externalIdSavedRef = useRef(conversationId != null)
|
||||||
const sessionIdRef = useRef<string | null>(null)
|
const sessionIdRef = useRef<string | null>(null)
|
||||||
const syncCancelRef = useRef<(() => void) | null>(null)
|
const syncCancelRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
@@ -477,6 +479,11 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
if (statusUpdatedRef.current) return
|
if (statusUpdatedRef.current) return
|
||||||
const persistedId = dbConvIdRef.current
|
const persistedId = dbConvIdRef.current
|
||||||
if (!persistedId) return
|
if (!persistedId) return
|
||||||
|
// Only update status if the user actually interacted in this session.
|
||||||
|
// A pure history view (opened from sidebar, no messages sent) should
|
||||||
|
// not flip the conversation to "completed" just because the ACP
|
||||||
|
// connection disconnected (e.g. agent auth expired).
|
||||||
|
if (!hasSentMessage) return
|
||||||
if (connStatus === "disconnected") {
|
if (connStatus === "disconnected") {
|
||||||
statusUpdatedRef.current = true
|
statusUpdatedRef.current = true
|
||||||
updateConversationLocal(persistedId, { status: "completed" })
|
updateConversationLocal(persistedId, { status: "completed" })
|
||||||
@@ -490,7 +497,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
console.error("[ConversationTabView] update status:", e)
|
console.error("[ConversationTabView] update status:", e)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [connStatus, updateConversationLocal])
|
}, [connStatus, hasSentMessage, updateConversationLocal])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dbConversationId == null) return
|
if (dbConversationId == null) return
|
||||||
|
|||||||
Reference in New Issue
Block a user