{"record":{"id":"8a8fca2913c6e210","repo":"chenhg5/cc-connect","slug":"session-is-closed-8a8fca","errorCode":null,"errorMessage":"session is closed","messagePattern":"session is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agent/qoder/session.go","lineNumber":92,"sourceCode":"\t}\n\n\tif resumeID != \"\" && resumeID != core.ContinueSession {\n\t\tqs.sessionID.Store(resumeID)\n\t}\n\n\treturn qs, nil\n}\n\nfunc (qs *qoderSession) Send(prompt string, messageID string, images []core.ImageAttachment, files []core.FileAttachment) error {\n\tif len(images) > 0 {\n\t\tslog.Warn(\"qoderSession: images not supported, ignoring\")\n\t}\n\tif len(files) > 0 {\n\t\tfilePaths := core.SaveFilesToDisk(qs.workDir, messageID, files)\n\t\tprompt = core.AppendFileRefs(prompt, filePaths)\n\t}\n\tif !qs.alive.Load() {\n\t\treturn fmt.Errorf(\"session is closed\")\n\t}\n\n\targs := append(append([]string{}, qs.extraArgs...), \"-p\", prompt, \"-f\", \"stream-json\", \"-q\", \"-w\", qs.workDir)\n\n\tsid := qs.CurrentSessionID()\n\tif sid != \"\" {\n\t\targs = append(args, \"-r\", sid)\n\t}\n\n\tif qs.mode == \"yolo\" {\n\t\tif os.Geteuid() == 0 {\n\t\t\tslog.Warn(\"qoderSession: --dangerously-skip-permissions not allowed under root, skipping flag\")\n\t\t} else {\n\t\t\targs = append(args, \"--dangerously-skip-permissions\")\n\t\t}\n\t}\n\n\tif qs.model != \"\" {","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/agent/qoder/session.go#L74-L110","documentation":"Send in agent/qoder/session.go rejects any message when the session's atomic alive flag is false, returning the fixed error \"session is closed\". The qoder session is one-shot: Send starts a new qoder process per message, and once Close() runs (or the session was never kept alive) further sends are invalid state transitions.","triggerScenarios":"Calling Send after Close() has been called on the session; sending on a session whose Close was triggered by teardown/cancellation; engine routing a queued message to an already-closed session.","commonSituations":"Message queued by the platform while the user ran /new or the session was stopped; a long-running turn completed and the session was closed before a follow-up message arrived; double-close then reuse.","solutions":["Check sess.Alive() before calling Send and create a new session if false","Ensure the engine creates a fresh session after Close instead of reusing the old one","Avoid racing Send with Close; serialize lifecycle transitions","Log the lifecycle (Close callers) to find who closed the session early"],"exampleFix":"// before\nerr := session.Send(ctx, msg)\n// after\nif !session.Alive() {\n    session, err = newQoderSession(...)\n}\nerr = session.Send(ctx, msg)","handlingStrategy":"validation","validationCode":"if !qs.Alive() {\n    qs, err = newQoderSession(...) // recreate before sending\n}","typeGuard":null,"tryCatchPattern":"if err := session.Send(ctx, msg); err != nil {\n    if err.Error() == \"session is closed\" {\n        session = recreateSession()\n        return session.Send(ctx, msg)\n    }\n    return err\n}","preventionTips":["Check Alive() before every Send","Never reuse a session after Close(); create a new one","Avoid sending from callbacks that can fire after teardown","Track session lifecycle transitions with logs"],"tags":["go","session","lifecycle","state"],"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"}