sipeed/picoclaw · error
session already completed
Error message
session already completed
What it means
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'.
Source
Thrown at pkg/tools/session.go:31
const maxOutputBufferSize = 1 * 1024 * 1024 // 1MB
const outputTruncateMarker = "\n... [output truncated, exceeded 1MB]\n"
// PtyKeyMode represents arrow key encoding mode for PTY sessions.
// Programs send smkx/rmkx sequences to switch between CSI and SS3 modes.
type PtyKeyMode uint8
const (
PtyKeyModeCSI PtyKeyMode = iota // triggered by rmkx (\x1b[?1l)
PtyKeyModeSS3 // triggered by smkx (\x1b[?1h)
)
const PtyKeyModeNotFound PtyKeyMode = 255
var (
ErrSessionNotFound = errors.New("session not found")
ErrSessionDone = errors.New("session already completed")
ErrPTYNotSupported = errors.New("PTY is not supported on this platform")
ErrNoStdin = errors.New("no stdin available")
)
type ProcessSession struct {
mu sync.Mutex
ID string
PID int
Command string
PTY bool
Background bool
StartTime int64
ExitCode int
Status string
stdinWriter io.Writer
stdoutPipe io.Reader
outputBuffer *bytes.Buffer
outputTruncated boolView on GitHub (pinned to 49183d7e8d)
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)
Example fix
// before
if err := sess.Write("q\n"); err != nil {
return err
}
// after
if err := sess.Write("q\n"); err != nil {
if errors.Is(err, tools.ErrSessionDone) {
return nil // program already exited; quit request is moot
}
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
if !sess.IsRunning() { // or sess.Status == "running" under lock
return nil // nothing to write/kill; process already exited
} Type guard
func isSessionDone(err error) bool {
return errors.Is(err, tools.ErrSessionDone)
} Try / catch
if err := sess.Write(data); err != nil {
if errors.Is(err, tools.ErrSessionDone) {
log.Printf("session %s already exited; dropping input", id)
return nil
}
if errors.Is(err, tools.ErrNoStdin) {
return fmt.Errorf("session %s has no stdin pipe", id)
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- session not found
- failed to load MCP servers: %w
- create process hook stdin: %w
- create process hook stdout: %w
- create process hook stderr: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/63097b41525df02d.
Report an issue: GitHub.