fix(forwarder): preserve Cursor fork context

Store checkpoint turns as validated blobs and restore prefetched parent turns so forked conversations retain their replay history and rewind prefix.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DedSecer
2026-08-01 10:51:23 +08:00
co-authored by Cursor
parent 00f52166b2
commit 14b286a466
14 changed files with 1979 additions and 304 deletions
+25 -30
View File
@@ -45,13 +45,25 @@ func (snapshot turnUsageSnapshot) requestTokensTotal() int64 {
return snapshot.promptTokensTotal() + nonNegativeInt64(snapshot.OutputTokens)
}
func (service *Service) importConversationState(item *ConversationFile, state *agentv1.ConversationStateStructure) ([]HistoryEntry, error) {
func (service *Service) importConversationState(item *ConversationFile, state *agentv1.ConversationStateStructure, prefetchedBlobs []*agentv1.PreFetchedBlob) ([]HistoryEntry, error) {
if item == nil || state == nil {
return nil, nil
}
blobs, err := newImportedBlobStore(prefetchedBlobs)
if err != nil {
return nil, err
}
importedIDs, err := importedTurnIDs(state.GetTurns(), blobs)
if err != nil {
return nil, err
}
item.TokenDetailsUsedTokens = state.GetTokenDetails().GetUsedTokens()
item.ImportedTurnIDs = importedIDs
if minimumNextTurnSeq := int64(len(item.ImportedTurnIDs)) + 1; item.NextTurnSeq < minimumNextTurnSeq {
item.NextTurnSeq = minimumNextTurnSeq
}
entries := make([]HistoryEntry, 0, 2)
if messages, err := importedConversationStateModelMessages(state); err != nil {
if messages, err := importedConversationStateModelMessages(state, blobs); err != nil {
return nil, err
} else {
for _, message := range messages {
@@ -104,7 +116,7 @@ func (service *Service) importConversationState(item *ConversationFile, state *a
return entries, nil
}
func importedConversationStateModelMessages(state *agentv1.ConversationStateStructure) ([]modeladapter.Message, error) {
func importedConversationStateModelMessages(state *agentv1.ConversationStateStructure, blobs importedBlobStore) ([]modeladapter.Message, error) {
if state == nil {
return nil, nil
}
@@ -113,7 +125,7 @@ func importedConversationStateModelMessages(state *agentv1.ConversationStateStru
if err != nil {
return nil, fmt.Errorf("decode imported replay messages: %w", err)
}
decoded = restoreImportedReplayUserMessages(decoded, state.GetTurns())
decoded = restoreImportedReplayUserMessages(decoded, state.GetTurns(), blobs)
decoded = filterLegacyPlainWriteReplay(decoded)
decoded = filterInternalPromptContextReplay(decoded)
messages := make([]modeladapter.Message, 0, len(decoded))
@@ -133,35 +145,18 @@ func importedConversationStateModelMessages(state *agentv1.ConversationStateStru
if len(rawTurn) == 0 {
continue
}
turn := &agentv1.ConversationTurnStructure{}
if err := proto.Unmarshal(rawTurn, turn); err != nil {
return nil, fmt.Errorf("decode imported turn: %w", err)
turn, turnID, err := decodeImportedTurn(rawTurn, blobs)
if err != nil {
return nil, err
}
agentTurn := turn.GetAgentConversationTurn()
if agentTurn == nil {
continue
if turn == nil && len(turnID) > 0 {
return nil, fmt.Errorf("missing prefetched turn blob %x", turnID)
}
if rawUser := agentTurn.GetUserMessage(); len(rawUser) > 0 {
userMessage := &agentv1.UserMessage{}
if err := proto.Unmarshal(rawUser, userMessage); err != nil {
return nil, fmt.Errorf("decode imported turn user_message: %w", err)
}
if replay, ok := promptengine.BuildUserMessageReplayMessage(userMessage); ok {
messages = append(messages, toModelMessage(replay))
}
}
for _, rawStep := range agentTurn.GetSteps() {
if len(rawStep) == 0 {
continue
}
step := &agentv1.ConversationStep{}
if err := proto.Unmarshal(rawStep, step); err != nil {
return nil, fmt.Errorf("decode imported turn step: %w", err)
}
for _, replay := range promptengine.BuildLegacyMessagesFromConversationStep(step) {
messages = append(messages, toModelMessage(replay))
}
turnMessages, err := importedBlobTurnMessages(turn, blobs)
if err != nil {
return nil, err
}
messages = append(messages, turnMessages...)
}
return normalizeReplayMessageSequence(messages), nil
}