From 1ac01f97c5d6fc6a89ab884a1bcc20f97518cd39 Mon Sep 17 00:00:00 2001 From: piyush0049 Date: Sat, 25 Jul 2026 21:03:25 +0530 Subject: [PATCH 1/3] fix(server): prevent send-on-closed-channel panic in generateTitle --- pkg/server/session_manager.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/server/session_manager.go b/pkg/server/session_manager.go index 5fe834f15..0e1c821f5 100644 --- a/pkg/server/session_manager.go +++ b/pkg/server/session_manager.go @@ -920,9 +920,13 @@ func (sm *SessionManager) RunSession(ctx context.Context, sessionID, agentFilena defer cancel() defer runtimeSession.streaming.Unlock() - // Start title generation in parallel if needed + // Start title generation in parallel if needed, coordinating via WaitGroup + // so close(streamChan) does not fire while generateTitle is still sending. + var wg sync.WaitGroup if needsTitle { - go sm.generateTitle(ctx, sess, titleGen, userMessages, streamChan) + wg.Go(func() { + sm.generateTitle(ctx, sess, titleGen, userMessages, streamChan) + }) } else if titleToEmit != "" { // Re-emit the existing title so late-joining SSE consumers // and boards can pick it up without an extra API call. @@ -932,11 +936,18 @@ func (sm *SessionManager) RunSession(ctx context.Context, sessionID, agentFilena stream := runtimeSession.runtime.RunStream(streamCtx, sess) for event := range stream { if streamCtx.Err() != nil { - return + break } streamChan <- event } + // Ensure title generation finishes before defers run close(streamChan). + wg.Wait() + + if streamCtx.Err() != nil { + return + } + if err := sm.sessionStore.UpdateSession(ctx, sess); err != nil { return } From fae2912c0c4838487a5330db2fd812bca7da86a5 Mon Sep 17 00:00:00 2001 From: piyush0049 Date: Wed, 5 Aug 2026 16:53:30 +0530 Subject: [PATCH 2/3] fix(server): add regression test and teardown note --- go.mod | 24 ++++----- go.sum | 48 ++++++++++-------- pkg/server/session_manager.go | 6 ++- pkg/server/session_manager_test.go | 81 ++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 35 deletions(-) diff --git a/go.mod b/go.mod index 551280694..4aa94278e 100644 --- a/go.mod +++ b/go.mod @@ -104,15 +104,15 @@ require ( github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/smartystreets/assertions v1.2.0 // indirect github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/tools v0.47.0 // indirect - google.golang.org/api v0.272.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/tools v0.48.0 // indirect + google.golang.org/api v0.292.0 // indirect ) require ( cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth v0.22.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect @@ -143,7 +143,7 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgageot/rubocop-go v0.0.0-20260627140528-ee9a9b36c3eb github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -164,8 +164,8 @@ require ( github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/safehtml v0.1.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect + github.com/googleapis/gax-go/v2 v2.23.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -189,7 +189,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.5.4 // indirect @@ -224,9 +224,9 @@ require ( golang.org/x/net v0.57.0 // indirect golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.81.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/grpc v1.83.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b1643c5af..407b79e89 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ= +cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -149,8 +149,9 @@ github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgageot/rubocop-go v0.0.0-20260627140528-ee9a9b36c3eb h1:E4a9ksSV9JrxAenB8/ByZI6LShO05uBEXzGEoe2DZE8= github.com/dgageot/rubocop-go v0.0.0-20260627140528-ee9a9b36c3eb/go.mod h1:wn4xlnesVpaaCT+xUYHAPW/rPxWd2jZIcUVqFkGUa0U= github.com/dgageot/ultraviolet v0.0.0-20260313154905-9451997d56b6 h1:88fWkkjwzuI4tRTqadbJIbA9O+gO67oyu+2OpHHuuT8= @@ -255,10 +256,10 @@ github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8 github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= -github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4= +github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= +github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= +github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -383,8 +384,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= @@ -486,8 +488,8 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= @@ -536,8 +538,8 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJk golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -575,22 +577,24 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/adk v1.2.0 h1:MfQD1/GqPfIsFNBcozNykkjdqNIdCrPH/SNqKPZF/yM= google.golang.org/adk v1.2.0/go.mod h1:6QY5jQI7awU4WYtJqvyIkJQheCvqsGWweU6BX63USEc= -google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= -google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/api v0.292.0 h1:Ewiwo/GTtiaPZSNAZQUcWLh8AYDEoPmIXyJfeoTSMHU= +google.golang.org/api v0.292.0/go.mod h1:07kjmMnFGm2RQuCza2EZM/5N68G/fVvFb1xKjWqoFA0= google.golang.org/genai v1.65.0 h1:6QK3Rjsx0iuJjbDCE1vf1VUr1IEjRDpvexGGnhxoAIk= google.golang.org/genai v1.65.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/pkg/server/session_manager.go b/pkg/server/session_manager.go index 0e1c821f5..5f5f800f6 100644 --- a/pkg/server/session_manager.go +++ b/pkg/server/session_manager.go @@ -924,6 +924,11 @@ func (sm *SessionManager) RunSession(ctx context.Context, sessionID, agentFilena // so close(streamChan) does not fire while generateTitle is still sending. var wg sync.WaitGroup if needsTitle { + // Note: generateTitle runs on the request ctx, so wg.Wait() only unblocks + // quickly when that context is cancelled. In the DeleteSession path + // (where the stream context is cancelled but the request context stays alive), + // the wait blocks until the title LLM call completes, delaying stream teardown. + // This bounded delay (first turn only) prioritizes persisting the title. wg.Go(func() { sm.generateTitle(ctx, sess, titleGen, userMessages, streamChan) }) @@ -941,7 +946,6 @@ func (sm *SessionManager) RunSession(ctx context.Context, sessionID, agentFilena streamChan <- event } - // Ensure title generation finishes before defers run close(streamChan). wg.Wait() if streamCtx.Err() != nil { diff --git a/pkg/server/session_manager_test.go b/pkg/server/session_manager_test.go index 595a565cf..1a5e01744 100644 --- a/pkg/server/session_manager_test.go +++ b/pkg/server/session_manager_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "io" "net/http" "net/http/httptest" "path/filepath" @@ -25,6 +26,9 @@ import ( "github.com/docker/docker-agent/pkg/concurrent" "github.com/docker/docker-agent/pkg/config" "github.com/docker/docker-agent/pkg/config/types" + "github.com/docker/docker-agent/pkg/model/provider" + "github.com/docker/docker-agent/pkg/model/provider/base" + "github.com/docker/docker-agent/pkg/modelsdev" "github.com/docker/docker-agent/pkg/runtime" "github.com/docker/docker-agent/pkg/session" "github.com/docker/docker-agent/pkg/session/sqlitestore" @@ -2301,3 +2305,80 @@ func TestExportSessionForRecovery_OmitsErrorsKeyWhenNone(t *testing.T) { _, present := export["errors"] assert.False(t, present) } + +type blockingStream struct { + chat.MessageStream + + blocker chan struct{} + returned bool +} + +func (s *blockingStream) Recv() (chat.MessageStreamResponse, error) { + if !s.returned { + <-s.blocker + s.returned = true + return chat.MessageStreamResponse{ + Choices: []chat.MessageStreamChoice{ + {Delta: chat.MessageDelta{Content: "A Generated Title"}}, + }, + }, nil + } + return chat.MessageStreamResponse{}, io.EOF +} + +func (s *blockingStream) Close() {} + +type blockingTitleProvider struct { + provider.Provider + + blocker chan struct{} +} + +func (p *blockingTitleProvider) ID() modelsdev.ID { return modelsdev.NewID("mock", "mock") } + +func (p *blockingTitleProvider) BaseConfig() base.Config { return base.Config{} } + +func (p *blockingTitleProvider) CreateChatCompletionStream(_ context.Context, _ []chat.Message, _ []tools.Tool) (chat.MessageStream, error) { + return &blockingStream{blocker: p.blocker}, nil +} + +func TestRunSession_GenerateTitleConcurrentClosePanic(t *testing.T) { + t.Parallel() + + ctx := t.Context() + sess := session.New() + + // Use fakeRuntime that ends the agent stream immediately by passing a nil release channel + fake := &fakeRuntime{} + sm := newTestSessionManager(t, sess, fake) + + blocker := make(chan struct{}) + mockProvider := &blockingTitleProvider{blocker: blocker} + titleGen := sessiontitle.New(mockProvider) + + // Inject the mock title generator into the active runtime + rt, _ := sm.runtimeSessions.Load(sess.ID) + rt.titleGen = titleGen + + // RunSession spawns generateTitle and fakeRuntime in parallel + streamChan, err := sm.RunSession(ctx, sess.ID, "agent", "root", []api.Message{{Content: "trigger title generation"}}, "") + require.NoError(t, err) + + go func() { + // Drain the stream. It will close once the title generator unblocks and wg.Wait() returns. + for range streamChan { + } + }() + + // Ensure the agent stream draining finishes, putting RunSession's defer stack + // in a position to execute if it weren't blocking on wg.Wait() + time.Sleep(50 * time.Millisecond) //nolint:forbidigo // giving the inner goroutine time to run past the fake stream and reach wg.Wait + + // Unblock the title generator. Without wg.Wait(), the streamChan would already + // be closed and this would panic on send in generateTitle. + close(blocker) + + // Test passes if it does not panic. Delete the session to clean up. + err = sm.DeleteSession(ctx, sess.ID) + require.NoError(t, err) +} From 1743cd152b3dcce20e5af6e70bcb665d01092187 Mon Sep 17 00:00:00 2001 From: piyush0049 Date: Wed, 5 Aug 2026 18:08:56 +0530 Subject: [PATCH 3/3] test: make credential helper tests cross-platform for windows --- pkg/environment/credential_helper_test.go | 24 ++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/environment/credential_helper_test.go b/pkg/environment/credential_helper_test.go index f7caccf28..43272a822 100644 --- a/pkg/environment/credential_helper_test.go +++ b/pkg/environment/credential_helper_test.go @@ -1,6 +1,7 @@ package environment import ( + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -17,6 +18,19 @@ func TestNewCredentialHelperProvider(t *testing.T) { func TestCredentialHelperProvider_Get(t *testing.T) { t.Parallel() + echoCmd := "echo" + echoArgs := func(v string) []string { return []string{v} } + falseCmd := "false" + + if runtime.GOOS == "windows" { + echoCmd = "powershell" + echoArgs = func(v string) []string { + return []string{"-NoProfile", "-Command", "Write-Output '" + v + "'"} + } + falseCmd = "powershell" + // simulate 'false' by exiting with 1 + } + tests := []struct { name string command string @@ -25,11 +39,11 @@ func TestCredentialHelperProvider_Get(t *testing.T) { wantValue string wantFound bool }{ - {"ignores non-DOCKER_TOKEN vars", "echo", []string{"test-token"}, "OTHER_VAR", "", false}, - {"success", "echo", []string{"my-secret-token"}, DockerDesktopTokenEnv, "my-secret-token", true}, - {"trims whitespace", "echo", []string{" token-with-spaces "}, DockerDesktopTokenEnv, "token-with-spaces", true}, - {"empty output", "echo", []string{""}, DockerDesktopTokenEnv, "", false}, - {"command fails", "false", nil, DockerDesktopTokenEnv, "", false}, + {"ignores non-DOCKER_TOKEN vars", echoCmd, echoArgs("test-token"), "OTHER_VAR", "", false}, + {"success", echoCmd, echoArgs("my-secret-token"), DockerDesktopTokenEnv, "my-secret-token", true}, + {"trims whitespace", echoCmd, echoArgs(" token-with-spaces "), DockerDesktopTokenEnv, "token-with-spaces", true}, + {"empty output", echoCmd, echoArgs(""), DockerDesktopTokenEnv, "", false}, + {"command fails", falseCmd, []string{"-NoProfile", "-Command", "exit 1"}, DockerDesktopTokenEnv, "", false}, {"command not found", "nonexistent-command-12345", nil, DockerDesktopTokenEnv, "", false}, }