{"record":{"id":"da06f19eab3aa152","repo":"chenhg5/cc-connect","slug":"session-is-closed-da06f1","errorCode":null,"errorMessage":"session is closed","messagePattern":"session is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agent/kimi/session.go","lineNumber":124,"sourceCode":"\t\t\targs = append(args, \"-r\", sid)\n\t\t} else {\n\t\t\targs = append(args, \"--resume\", sid)\n\t\t}\n\t}\n\tif ks.model != \"\" {\n\t\targs = append(args, \"--model\", ks.model)\n\t}\n\tif ks.workDir != \"\" && ks.flagSupport.WorkDir {\n\t\targs = append(args, \"--work-dir\", ks.workDir)\n\t}\n\n\targs = append(args, \"--prompt\", prompt)\n\treturn args\n}\n\nfunc (ks *kimiSession) Send(prompt string, messageID string, images []core.ImageAttachment, files []core.FileAttachment) error {\n\tif !ks.alive.Load() {\n\t\treturn fmt.Errorf(\"session is closed\")\n\t}\n\n\t// Save images and files into the workspace so Kimi CLI can access them.\n\tattachDir := filepath.Join(ks.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":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/agent/kimi/session.go#L106-L142","documentation":"kimiSession.Send checks an atomic `alive` flag before doing anything; if the underlying kimi CLI process has already exited (or the session was closed/Stop()ed), Send refuses to accept the prompt and returns `session is closed`. A kimiSession's lifetime is exactly the lifetime of its child process, so a dead process means the session can no longer accept prompts. This is thrown by cc-connect's agent/kimi adapter, not by the kimi CLI itself.","triggerScenarios":"Calling Send() on a kimiSession after: (1) the kimi CLI process exited (crash, OOM kill, or self-termination), (2) Close()/Stop() was called on the session, (3) the session context was cancelled which kills the process and flips alive=false, or (4) a cached session handle is reused after the process already ran to completion.","commonSituations":"A long-running chat where the kimi binary died silently between turns; reusing a stored AgentSession reference after a /new or /switch; a timer/cron job sending into a session that was concurrently torn down; a broken or outdated kimi CLI that exits immediately at startup.","solutions":["Do not cache AgentSession handles across turns; obtain the current session from the engine/SessionManager before each Send.","On 'session is closed', create a fresh session (StartSession with the same workDir) and resend the prompt.","Check kimi CLI health and logs — an early process exit is the root cause; this error is only the symptom.","If you manage lifecycle yourself, guard Send/Close concurrency and only Close() from one code path."],"exampleFix":"// before\nif err := sess.Send(prompt, msgID, nil, nil); err != nil {\n    return err // \"session is closed\" propagates to the user\n}\n\n// after\nif err := sess.Send(prompt, msgID, nil, nil); err != nil {\n    if strings.Contains(err.Error(), \"session is closed\") {\n        sess, err = agent.StartSession(ctx, opts) // recreate dead session\n        if err != nil {\n            return err\n        }\n        return sess.Send(prompt, msgID, nil, nil)\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// Go returns errors, not throws; pre-check via session info if exposed\n// e.g. only send to sessions returned by an up-to-date ListSessions call\ninfo, err := agent.ListSessions(ctx)\nif err == nil && !containsID(info, currentID) {\n    sess, err = agent.StartSession(ctx, opts) // stale session, recreate first\n}","typeGuard":"func isSessionClosed(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"session is closed\")\n}","tryCatchPattern":"if err := sess.Send(prompt, msgID, nil, nil); err != nil {\n    if isSessionClosed(err) {\n        sess, err = agent.StartSession(ctx, opts)\n        if err != nil { return err }\n        err = sess.Send(prompt, msgID, nil, nil)\n    }\n    if err != nil { return err }\n}","preventionTips":["Never cache AgentSession handles across turns; resolve the current session each time","Consume the session's Events() channel so process-death EventErrors are observed promptly","Monitor kimi CLI crashes (stderr/OOM) which are the usual root cause of dead sessions","Serialize lifecycle operations (Start/Stop/Send) to avoid races"],"tags":["go","process-lifecycle","session"],"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"}