chenhg5/cc-connect · error

piSession: write stdin: %w

Error message

piSession: write stdin: %w

What it means

This error is produced by writeRPCCommand in agent/pi/session.go when writing a JSON-RPC command line to the stdin of the persistent pi RPC child process fails. The wrapped os.ErrClosed or broken-pipe error is included via %w, so the underlying cause is always the child's stdin pipe. The pi adapter keeps one long-lived RPC process per session and serializes all stdin writes through rpcStdinMu; this error means that channel to the child is unusable.

Source

Thrown at agent/pi/session.go:462

	return nil
}

// writeRPCCommand marshals cmd as a single JSONL line and writes it to the
// RPC process's stdin under rpcStdinMu. Used by both sendRPC (for "prompt"
// commands during a turn) and startRPC (for the startup "get_state" probe
// that fetches the session id before callers are released).
func (s *piSession) writeRPCCommand(cmd map[string]any) error {
	b, err := json.Marshal(cmd)
	if err != nil {
		return fmt.Errorf("piSession: marshal command: %w", err)
	}
	b = append(b, '\n')

	s.rpcStdinMu.Lock()
	_, err = s.rpcStdin.Write(b)
	s.rpcStdinMu.Unlock()
	if err != nil {
		return fmt.Errorf("piSession: write stdin: %w", err)
	}
	return nil
}

// sendRPC writes a JSON "prompt" command to the persistent RPC process stdin.
// Events are read asynchronously by readLoopRPC, including agent_end which
// triggers EventResult.
//
// Issue #1723: image paths are embedded into the message text as
// @<path> references (pi's standard mechanism, parsed the same way as in
// json mode). The rpc stdin pipe doesn't crash on NUL bytes, but the
// model still needs to load the images from disk — embedding raw bytes
// in the message field would give the model text-shaped garbage instead
// of a real visual input.
//
// Issue #1767: filePaths (non-image attachments) are NOT @<path>'d.
// pi's processFileArguments would inline their full UTF-8 contents into
// the message text the model sees; we append a plain "Files saved

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Check the wrapped cause with errors.Is(err, os.ErrClosed) / io.ErrClosedPipe to confirm the child process is gone
  2. Recreate the pi session (start a new RPC process) instead of retrying the write
  3. Log/inspect why the pi process exited (stderr, exit status) before restarting
  4. Guard Send with a session-alive check so callers get a clearer 'session closed' error

Example fix

// before
if err := session.Send(ctx, msg); err != nil {
    return fmt.Errorf("send failed: %w", err)
}
// after
if err := session.Send(ctx, msg); err != nil {
    if errors.Is(err, os.ErrClosed) || errors.Is(err, io.ErrClosedPipe) {
        session, err = pi.NewSession(opts) // restart RPC process
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Go: check liveness before writing
if !sessionAlive() {
    return errors.New("pi session closed")
}

Type guard

func isPipeClosed(err error) bool {
    return errors.Is(err, os.ErrClosed) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, syscall.EPIPE)
}

Try / catch

if err := writeRPCCommand(cmd); err != nil {
    if isPipeClosed(err) {
        // child process is gone; restart session and resend once
        return restartAndRetry(cmd)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Send (via sendRPC) or starting the session (via startRPC) after the pi process has exited or its stdin pipe was closed; writes racing with Close(); the child crashing mid-session so the pipe is broken.

Common situations: The pi CLI binary crashed or was killed (OOM, signal) while the session object still looked alive; a turn was cancelled and teardown closed stdin while a send was in flight; a stale session reused after process exit.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/3e549602928153bc. Report an issue: GitHub.