@@ -1226,9 +1226,10 @@ func parseCodexSession(filePath string) (SyncSession, bool) {
12261226 }
12271227 content := parseMessageContent (payload ["content" ])
12281228 if content != "" {
1229+ role := normalizeStructuredMessageRole (stringFrom (payload ["role" ]), payload ["content" ])
12291230 session .Messages = append (session .Messages , SyncMessage {
12301231 Index : msgIndex ,
1231- Role : stringFrom ( payload [ " role" ]) ,
1232+ Role : role ,
12321233 Content : content ,
12331234 })
12341235 msgIndex ++
@@ -3849,10 +3850,7 @@ func parseAmpThread(path string) (SyncSession, bool) {
38493850 msgIndex := 0
38503851 for _ , item := range entries {
38513852 msg := mapFrom (item )
3852- role := strings .TrimSpace (stringFrom (msg ["role" ]))
3853- if role == "" {
3854- continue
3855- }
3853+ role := normalizeStructuredMessageRole (stringFrom (msg ["role" ]), msg ["content" ])
38563854
38573855 tsMs := int64From (mapFrom (msg ["meta" ])["sentAt" ], 0 )
38583856 if tsMs > 0 {
@@ -5070,10 +5068,7 @@ func parseDroidSession(path string) (SyncSession, bool) {
50705068 if content == "" {
50715069 continue
50725070 }
5073- role := message .Role
5074- if role == "" {
5075- role = "user"
5076- }
5071+ role := normalizeStructuredMessageRole (message .Role , message .Content )
50775072 timestamp := stringFrom (item ["timestamp" ])
50785073 session .Messages = append (session .Messages , SyncMessage {
50795074 Index : msgIndex ,
@@ -5253,13 +5248,7 @@ func parseQoderSession(path string) (SyncSession, bool) {
52535248 }
52545249
52555250 messageObj := mapFrom (item ["message" ])
5256- role := strings .TrimSpace (stringFrom (messageObj ["role" ]))
5257- if role == "" {
5258- role = strings .TrimSpace (stringFrom (item ["type" ]))
5259- }
5260- if role == "" {
5261- role = "assistant"
5262- }
5251+ role := resolveQoderRole (item , messageObj )
52635252 content := parseQoderContent (messageObj ["content" ])
52645253 if content == "" {
52655254 continue
@@ -5339,6 +5328,59 @@ func parseQoderContent(value any) string {
53395328 }
53405329}
53415330
5331+ func resolveQoderRole (item map [string ]any , messageObj map [string ]any ) string {
5332+ role := strings .ToLower (strings .TrimSpace (stringFrom (messageObj ["role" ])))
5333+ if role == "" {
5334+ role = strings .ToLower (strings .TrimSpace (stringFrom (item ["type" ])))
5335+ }
5336+ return normalizeStructuredMessageRole (role , messageObj ["content" ])
5337+ }
5338+
5339+ func normalizeStructuredMessageRole (role string , content any ) string {
5340+ normalizedRole := strings .ToLower (strings .TrimSpace (role ))
5341+ hasText , hasToolUse , hasToolResult := structuredContentFlags (content )
5342+ // Some tools encode tool events as user role; force these to assistant-side turns.
5343+ if hasToolResult && ! hasText && ! hasToolUse {
5344+ return "assistant"
5345+ }
5346+ if hasToolUse {
5347+ return "assistant"
5348+ }
5349+ switch normalizedRole {
5350+ case "user" , "assistant" , "system" , "tool" :
5351+ return normalizedRole
5352+ case "" :
5353+ return "assistant"
5354+ default :
5355+ return normalizedRole
5356+ }
5357+ }
5358+
5359+ func structuredContentFlags (value any ) (hasText bool , hasToolUse bool , hasToolResult bool ) {
5360+ items , ok := value .([]any )
5361+ if ! ok {
5362+ return false , false , false
5363+ }
5364+ for _ , item := range items {
5365+ entry , ok := item .(map [string ]any )
5366+ if ! ok {
5367+ continue
5368+ }
5369+ entryType := strings .ToLower (strings .TrimSpace (stringFrom (entry ["type" ])))
5370+ switch entryType {
5371+ case "text" :
5372+ if strings .TrimSpace (stringFrom (entry ["text" ])) != "" {
5373+ hasText = true
5374+ }
5375+ case "tool_use" :
5376+ hasToolUse = true
5377+ case "tool_result" :
5378+ hasToolResult = true
5379+ }
5380+ }
5381+ return hasText , hasToolUse , hasToolResult
5382+ }
5383+
53425384func parseClaudeContent (value any ) string {
53435385 switch v := value .(type ) {
53445386 case string :
0 commit comments