feat(cursor): sync local conversation history to Cursor transcripts

Project sidecar context history into Cursor-compatible JSONL transcripts so previous conversations remain readable when referenced from new chats. Backfill existing sessions on startup and preserve Cursor-managed turn status entries during atomic updates.
This commit is contained in:
DedSecer
2026-08-01 21:08:14 +08:00
parent 00f52166b2
commit 68e63c7a72
6 changed files with 749 additions and 1 deletions
+77 -1
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
@@ -438,7 +439,11 @@ func (store *ConversationFileStore) writeConversationLocked(conversationID strin
if err := store.writeContextLocked(conversationID, conversation); err != nil {
return err
}
return store.writeConversationMetaLocked(conversationID, conversation)
if err := store.writeConversationMetaLocked(conversationID, conversation); err != nil {
return err
}
store.syncCursorTranscriptBestEffort(conversationID, conversation)
return nil
}
func (store *ConversationFileStore) writeConversationMetaLocked(conversationID string, conversation *ConversationFile) error {
@@ -477,6 +482,70 @@ func (store *ConversationFileStore) writeContextLocked(conversationID string, co
return writeJSONFileAtomic(store.contextPath(conversationID), context)
}
func (store *ConversationFileStore) syncCursorTranscriptBestEffort(conversationID string, conversation *ConversationFile) {
if store == nil || conversation == nil {
return
}
folder := normalizeAgentTranscriptsFolder(conversation.AgentTranscriptsFolder)
if folder == "" {
return
}
if err := store.syncCursorTranscript(conversationID, conversation, folder); err != nil {
log.Printf("forwarder transcript sync failed conversation_id=%s err=%v", strings.TrimSpace(conversationID), err)
}
}
func (store *ConversationFileStore) syncCursorTranscript(conversationID string, conversation *ConversationFile, transcriptsFolder string) error {
return store.syncCursorTranscriptWithLatestStatus(conversationID, conversation, transcriptsFolder, false)
}
func (store *ConversationFileStore) syncCursorTranscriptWithLatestStatus(conversationID string, conversation *ConversationFile, transcriptsFolder string, includeLatestStatus bool) error {
if store == nil || conversation == nil {
return nil
}
path, err := cursorTranscriptPath(transcriptsFolder, conversationID)
if err != nil {
return err
}
data, err := projectCursorTranscriptJSONLWithLatestStatus(conversation, includeLatestStatus)
if err != nil {
return err
}
if len(data) == 0 {
return nil
}
data = preserveCursorAppendedTurnEnded(path, data)
return writeCursorTranscriptAtomic(path, data)
}
func (store *ConversationFileStore) SyncAllCursorTranscriptsBestEffort() {
if store == nil {
return
}
conversationIDs, err := store.ListConversationIDs()
if err != nil {
log.Printf("forwarder transcript backfill scan failed err=%v", err)
return
}
for _, conversationID := range conversationIDs {
conversation, err := store.LoadConversation(conversationID)
if err != nil {
log.Printf("forwarder transcript backfill load failed conversation_id=%s err=%v", conversationID, err)
continue
}
if conversation == nil || conversation.AgentTranscriptsFolder == "" {
continue
}
info, err := os.Stat(conversation.AgentTranscriptsFolder)
if err != nil || !info.IsDir() {
continue
}
if err := store.syncCursorTranscriptWithLatestStatus(conversationID, conversation, conversation.AgentTranscriptsFolder, true); err != nil {
log.Printf("forwarder transcript backfill failed conversation_id=%s err=%v", conversationID, err)
}
}
}
func contextVersionForEntries(entries []HistoryEntry) int64 {
var version int64
for _, entry := range entries {
@@ -663,6 +732,9 @@ func mergeConversationMetadata(target *ConversationFile, source *ConversationFil
target.ParentConversationID = strings.TrimSpace(source.ParentConversationID)
target.ParentToolCallID = strings.TrimSpace(source.ParentToolCallID)
target.SubagentTypeName = strings.TrimSpace(source.SubagentTypeName)
if folder := normalizeAgentTranscriptsFolder(source.AgentTranscriptsFolder); folder != "" {
target.AgentTranscriptsFolder = folder
}
if strings.TrimSpace(source.Mode) != "" {
target.Mode = strings.TrimSpace(source.Mode)
}
@@ -718,6 +790,10 @@ func normalizeLoadedConversation(conversationID string, conversation *Conversati
if conversation.Entries == nil {
conversation.Entries = make([]HistoryEntry, 0, 16)
}
conversation.AgentTranscriptsFolder = normalizeAgentTranscriptsFolder(conversation.AgentTranscriptsFolder)
if conversation.AgentTranscriptsFolder == "" {
conversation.AgentTranscriptsFolder = agentTranscriptsFolderFromEntries(conversation.Entries)
}
for _, entry := range conversation.Entries {
if entry.Seq >= conversation.NextEntrySeq {
conversation.NextEntrySeq = entry.Seq + 1