{"record":{"id":"63097b41525df02d","repo":"sipeed/picoclaw","slug":"session-already-completed","errorCode":null,"errorMessage":"session already completed","messagePattern":"session already completed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/tools/session.go","lineNumber":31,"sourceCode":"\nconst maxOutputBufferSize = 1 * 1024 * 1024 // 1MB\n\nconst outputTruncateMarker = \"\\n... [output truncated, exceeded 1MB]\\n\"\n\n// PtyKeyMode represents arrow key encoding mode for PTY sessions.\n// Programs send smkx/rmkx sequences to switch between CSI and SS3 modes.\ntype PtyKeyMode uint8\n\nconst (\n\tPtyKeyModeCSI PtyKeyMode = iota // triggered by rmkx (\\x1b[?1l)\n\tPtyKeyModeSS3                   // triggered by smkx (\\x1b[?1h)\n)\n\nconst PtyKeyModeNotFound PtyKeyMode = 255\n\nvar (\n\tErrSessionNotFound = errors.New(\"session not found\")\n\tErrSessionDone     = errors.New(\"session already completed\")\n\tErrPTYNotSupported = errors.New(\"PTY is not supported on this platform\")\n\tErrNoStdin         = errors.New(\"no stdin available\")\n)\n\ntype ProcessSession struct {\n\tmu              sync.Mutex\n\tID              string\n\tPID             int\n\tCommand         string\n\tPTY             bool\n\tBackground      bool\n\tStartTime       int64\n\tExitCode        int\n\tStatus          string\n\tstdinWriter     io.Writer\n\tstdoutPipe      io.Reader\n\toutputBuffer    *bytes.Buffer\n\toutputTruncated bool","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/tools/session.go#L13-L49","documentation":"Sentinel error meaning the target ProcessSession is no longer in the \"running\" state. killProcess returns it when Status != \"running\" (session.go:102-104) and Write returns it for the same check (session.go:128-130), i.e. any attempt to kill or feed stdin to a process that already exited or was killed. shell.go (lines 829, 1093) maps it to a friendly message such as 'session already completed'.","triggerScenarios":"Calling Write(data) after the child process exited (EOF on stdin); calling Kill() twice; writing to a session whose ExitCode was already set by the wait goroutine.","commonSituations":"Long-running commands (watch, tail -f, servers) that exit on their own while the agent still holds the session ID; double-kill during cleanup; PTY programs that exit on Ctrl-D before subsequent input is sent.","solutions":["Treat it as success in cleanup paths: the goal (process not running) is already achieved","Check sess.Status == \"running\" (under the session mutex or via an accessor) before Write/Kill","For interactive programs, drain output/Wait before sending more input so exit is detected first","In tool wrappers, convert to an informational message via errors.Is(err, tools.ErrSessionDone)"],"exampleFix":"// before\nif err := sess.Write(\"q\\n\"); err != nil {\n    return err\n}\n\n// after\nif err := sess.Write(\"q\\n\"); err != nil {\n    if errors.Is(err, tools.ErrSessionDone) {\n        return nil // program already exited; quit request is moot\n    }\n    return err\n}","handlingStrategy":"type-guard","validationCode":"if !sess.IsRunning() { // or sess.Status == \"running\" under lock\n    return nil // nothing to write/kill; process already exited\n}","typeGuard":"func isSessionDone(err error) bool {\n    return errors.Is(err, tools.ErrSessionDone)\n}","tryCatchPattern":"if err := sess.Write(data); err != nil {\n    if errors.Is(err, tools.ErrSessionDone) {\n        log.Printf(\"session %s already exited; dropping input\", id)\n        return nil\n    }\n    if errors.Is(err, tools.ErrNoStdin) {\n        return fmt.Errorf(\"session %s has no stdin pipe\", id)\n    }\n    return err\n}","preventionTips":["Drain session output before sending more input so exits are noticed first","Treat ErrSessionDone as success in Kill/cleanup logic","In long-lived agents, poll Status between interactions with interactive programs","Guard double-kill paths (signal handler + explicit cleanup) with a done-check"],"tags":["go","process","session","lifecycle"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}