@@ -15,9 +15,9 @@ import (
1515 "neo-code/internal/config"
1616 agentcontext "neo-code/internal/context"
1717 contextcompact "neo-code/internal/context/compact"
18- "neo-code/internal/repository"
1918 "neo-code/internal/provider"
2019 providertypes "neo-code/internal/provider/types"
20+ "neo-code/internal/repository"
2121 approvalflow "neo-code/internal/runtime/approval"
2222 "neo-code/internal/runtime/controlplane"
2323 "neo-code/internal/runtime/streaming"
@@ -3594,6 +3594,108 @@ func TestServiceListSessionsSkipsPromotionWhenDerivedTitleInvalid(t *testing.T)
35943594 }
35953595}
35963596
3597+ func TestServiceLoadSessionRepairsIncompleteToolCallTail (t * testing.T ) {
3598+ manager := newRuntimeConfigManager (t )
3599+ store := newMemoryStore ()
3600+ service := NewWithFactory (manager , tools .NewRegistry (), store , nil , nil )
3601+
3602+ session := agentsession .New ("Repair Me" )
3603+ session .Messages = []providertypes.Message {
3604+ {Role : providertypes .RoleUser , Parts : []providertypes.ContentPart {providertypes .NewTextPart ("before" )}},
3605+ {
3606+ Role : providertypes .RoleAssistant ,
3607+ ToolCalls : []providertypes.ToolCall {
3608+ {ID : "call-1" , Name : "filesystem_read_file" , Arguments : `{"path":"README.md"}` },
3609+ {ID : "call-2" , Name : "bash" , Arguments : `{"command":"echo hi"}` },
3610+ },
3611+ },
3612+ {
3613+ Role : providertypes .RoleTool ,
3614+ ToolCallID : "call-1" ,
3615+ Parts : []providertypes.ContentPart {providertypes .NewTextPart ("README" )},
3616+ },
3617+ }
3618+ store .sessions [session .ID ] = cloneSession (session )
3619+
3620+ loaded , err := service .LoadSession (context .Background (), session .ID )
3621+ if err != nil {
3622+ t .Fatalf ("LoadSession() error = %v" , err )
3623+ }
3624+ if len (loaded .Messages ) != 1 {
3625+ t .Fatalf ("len(loaded.Messages) = %d, want 1" , len (loaded .Messages ))
3626+ }
3627+ if got := renderPartsForTest (loaded .Messages [0 ].Parts ); got != "before" {
3628+ t .Fatalf ("loaded preserved message = %q, want %q" , got , "before" )
3629+ }
3630+
3631+ persisted , err := store .LoadSession (context .Background (), session .ID )
3632+ if err != nil {
3633+ t .Fatalf ("store.LoadSession() error = %v" , err )
3634+ }
3635+ if len (persisted .Messages ) != 1 {
3636+ t .Fatalf ("len(persisted.Messages) = %d, want 1" , len (persisted .Messages ))
3637+ }
3638+ }
3639+
3640+ func TestServiceRunRepairsIncompleteToolCallTailBeforeBuildingContext (t * testing.T ) {
3641+ manager := newRuntimeConfigManager (t )
3642+ store := newMemoryStore ()
3643+ builder := & stubContextBuilder {}
3644+ scripted := & scriptedProvider {
3645+ responses : []scriptedResponse {
3646+ {
3647+ Message : providertypes.Message {
3648+ Role : providertypes .RoleAssistant ,
3649+ Parts : []providertypes.ContentPart {providertypes .NewTextPart ("done" )},
3650+ },
3651+ FinishReason : "stop" ,
3652+ },
3653+ },
3654+ }
3655+ service := NewWithFactory (manager , tools .NewRegistry (), store , & scriptedProviderFactory {provider : scripted }, builder )
3656+
3657+ session := agentsession .New ("Repair Before Run" )
3658+ session .Messages = []providertypes.Message {
3659+ {Role : providertypes .RoleUser , Parts : []providertypes.ContentPart {providertypes .NewTextPart ("before" )}},
3660+ {
3661+ Role : providertypes .RoleAssistant ,
3662+ ToolCalls : []providertypes.ToolCall {
3663+ {ID : "call-1" , Name : "filesystem_read_file" , Arguments : `{"path":"README.md"}` },
3664+ },
3665+ },
3666+ }
3667+ store .sessions [session .ID ] = cloneSession (session )
3668+
3669+ if err := service .Run (context .Background (), UserInput {
3670+ SessionID : session .ID ,
3671+ RunID : "run-repair-incomplete-tool-tail" ,
3672+ Parts : []providertypes.ContentPart {providertypes .NewTextPart ("continue" )},
3673+ }); err != nil {
3674+ t .Fatalf ("Run() error = %v" , err )
3675+ }
3676+
3677+ if len (builder .lastInput .Messages ) != 2 {
3678+ t .Fatalf ("len(builder.lastInput.Messages) = %d, want 2" , len (builder .lastInput .Messages ))
3679+ }
3680+ if builder .lastInput .Messages [0 ].Role != providertypes .RoleUser || renderPartsForTest (builder .lastInput .Messages [0 ].Parts ) != "before" {
3681+ t .Fatalf ("unexpected repaired history in builder input: %+v" , builder .lastInput .Messages )
3682+ }
3683+ if builder .lastInput .Messages [1 ].Role != providertypes .RoleUser || renderPartsForTest (builder .lastInput .Messages [1 ].Parts ) != "continue" {
3684+ t .Fatalf ("expected latest user input in builder messages, got %+v" , builder .lastInput .Messages )
3685+ }
3686+
3687+ persisted , err := store .LoadSession (context .Background (), session .ID )
3688+ if err != nil {
3689+ t .Fatalf ("store.LoadSession() error = %v" , err )
3690+ }
3691+ if len (persisted .Messages ) < 2 {
3692+ t .Fatalf ("expected repaired transcript plus new turn, got %+v" , persisted .Messages )
3693+ }
3694+ if len (persisted .Messages [0 ].ToolCalls ) != 0 {
3695+ t .Fatalf ("expected repaired transcript to drop dangling tool_calls, got %+v" , persisted .Messages [0 ])
3696+ }
3697+ }
3698+
35973699func TestRuntimeSessionTitlePromotionHelpers (t * testing.T ) {
35983700 t .Parallel ()
35993701
0 commit comments