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 bool

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Treat it as success in cleanup paths: the goal (process not running) is already achieved
  2. Check sess.Status == "running" (under the session mutex or via an accessor) before Write/Kill
  3. For interactive programs, drain output/Wait before sending more input so exit is detected first
  4. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/63097b41525df02d. Report an issue: GitHub.