{"record":{"id":"3fb14ba88af8a725","repo":"chenhg5/cc-connect","slug":"session-is-closed-3fb14b","errorCode":null,"errorMessage":"session is closed","messagePattern":"session is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agent/gemini/session.go","lineNumber":71,"sourceCode":"\t\tmode:      mode,\n\t\ttimeout:   timeout,\n\t\textraEnv:  extraEnv,\n\t\tevents:    make(chan core.Event, 64),\n\t\tctx:       sessionCtx,\n\t\tcancel:    cancel,\n\t}\n\tgs.alive.Store(true)\n\n\tif resumeID != \"\" && resumeID != core.ContinueSession {\n\t\tgs.chatID.Store(resumeID)\n\t}\n\n\treturn gs, nil\n}\n\nfunc (gs *geminiSession) Send(prompt string, messageID string, images []core.ImageAttachment, files []core.FileAttachment) (err error) {\n\tif !gs.alive.Load() {\n\t\treturn fmt.Errorf(\"session is closed\")\n\t}\n\n\t// Save images and files into the workspace so Gemini CLI tools can access them.\n\tattachDir := filepath.Join(gs.workDir, \".cc-connect\", \"attachments\")\n\tif (len(images) > 0 || len(files) > 0) && os.MkdirAll(attachDir, 0o755) != nil {\n\t\tattachDir = os.TempDir()\n\t}\n\n\tvar imageRefs []string\n\tfor i, img := range images {\n\t\text := \".png\"\n\t\tswitch img.MimeType {\n\t\tcase \"image/jpeg\":\n\t\t\text = \".jpg\"\n\t\tcase \"image/gif\":\n\t\t\text = \".gif\"\n\t\tcase \"image/webp\":\n\t\t\text = \".webp\"","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/agent/gemini/session.go#L53-L89","documentation":"geminiSession.Send checks the session's atomic `alive` flag before doing anything; if the session has been closed or its process exited, it rejects the prompt with `session is closed`. This prevents sending into a dead process and surfaces stale-session usage early.","triggerScenarios":"Sending a prompt on a geminiSession after the CLI process exited (crash, timeout, natural completion), after an explicit close/Stop, or after the context was cancelled — any state where alive.Load() is false.","commonSituations":"Engine keeps a session reference after the gemini CLI timed out; user resumes a session that hit the configured timeout; crash of the CLI marked the session dead but the UI still routes messages to it.","solutions":["Check session liveness/events before sending, or catch this error and start a new session","Inspect logs for why the previous process exited (timeout, crash, stderr output)","Increase timeoutMins in the gemini agent config if sessions expire too early","Implement automatic session recreation on this error in the calling layer"],"exampleFix":"// before\nerr := sess.Send(prompt, msgID, nil, nil) // fails: session is closed\n// after\nif err := sess.Send(prompt, msgID, nil, nil); err != nil && strings.Contains(err.Error(), \"session is closed\") {\n    sess, err = agent.StartSession(ctx, workDir, model)\n    if err == nil { err = sess.Send(prompt, msgID, nil, nil) }\n}","handlingStrategy":"validation","validationCode":"// track liveness via events before sending\nclosed := false\ngo func() { for evt := range sess.Events() { if evt.Type == core.EventError || evt.Type == core.EventExit { closed = true } } }()\nif closed { sess, _ = agent.StartSession(ctx, workDir, model) }","typeGuard":null,"tryCatchPattern":"if err := sess.Send(prompt, id, nil, nil); err != nil && err.Error() == \"session is closed\" {\n  sess, serr := agent.StartSession(ctx, workDir, model)\n  if serr != nil { return serr }\n  return sess.Send(prompt, id, nil, nil)\n}","preventionTips":["Watch the session's Events channel for exit/error to drop stale references","Recreate sessions proactively after timeouts or CLI crashes","Set a generous timeoutMins in config for long tasks"],"tags":["session","lifecycle","gemini-agent"],"backgroundTag":"invalid-state-transition","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}