Simplify session lifecycle and MCP cleanup

This commit is contained in:
2026-05-14 20:51:37 +01:00
parent 27361f79c4
commit cc4bf9e904
16 changed files with 439 additions and 255 deletions

View File

@@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
@@ -88,6 +89,7 @@ type Child struct {
ptyMu sync.RWMutex
pty *pkgpty.PTY
em *vt.GhosttyEmulator
runID uint64
status atomic.Pointer[ChildStatus]
exitCode atomic.Int32
@@ -115,6 +117,10 @@ type Child struct {
// portsMu guards ports. Best-effort port detection: regex on stream.
portsMu sync.Mutex
ports []PortSighting
cleanupMu sync.Mutex
cleanupPaths []string
restarting atomic.Bool
}
// PortSighting is one entry returned by get_process_ports.
@@ -126,10 +132,7 @@ type PortSighting struct {
const ringCap = 1 << 20 // 1 MiB per SPEC §5
// newChildEntry builds the in-memory Child record but does NOT start a
// PTY. Used so command entries can exist in the `stopped` state from the
// moment they're created. Agents and terminals call newChild() which
// chains newChildEntry + startPTY for the initial run.
// newChildEntry builds the in-memory Child record but does NOT start a PTY.
func newChildEntry(id, name string, kind ChildKind, argv, env []string, parentID, workDir, presetRef string) *Child {
c := &Child{
ID: id,
@@ -156,25 +159,14 @@ func newChildEntry(id, name string, kind ChildKind, argv, env []string, parentID
return c
}
func newChild(id, name string, kind ChildKind, argv, env []string, cols, rows uint16, parentID, workDir, presetRef string) (*Child, error) {
if len(argv) == 0 {
return nil, errors.New("child: empty argv")
}
c := newChildEntry(id, name, kind, argv, env, parentID, workDir, presetRef)
if err := c.startPTY(cols, rows); err != nil {
return nil, err
}
return c, nil
}
// startPTY (re)builds the emulator + PTY for this entry. Called by
// newChild on initial spawn and by Restart on subsequent runs. The
// status transitions stopped/exited → starting → running. On error the
// entry returns to errored.
func (c *Child) startPTY(cols, rows uint16) error {
func (c *Child) startPTY(cols, rows uint16) (uint64, error) {
em, err := vt.NewGhosttyEmulator(cols, rows)
if err != nil {
return fmt.Errorf("child %s emulator: %w", c.ID, err)
return 0, fmt.Errorf("child %s emulator: %w", c.ID, err)
}
starting := StatusStarting
c.status.Store(&starting)
@@ -183,12 +175,14 @@ func (c *Child) startPTY(cols, rows uint16) error {
em.Close()
errored := StatusErrored
c.status.Store(&errored)
return fmt.Errorf("child %s pty: %w", c.ID, err)
return 0, fmt.Errorf("child %s pty: %w", c.ID, err)
}
em.OnWritePTY(func(b []byte) {
_, _ = p.Write(b)
})
c.ptyMu.Lock()
c.runID++
runID := c.runID
c.pty = p
c.em = em
c.ptyMu.Unlock()
@@ -196,7 +190,7 @@ func (c *Child) startPTY(cols, rows uint16) error {
c.status.Store(&running)
c.exitCode.Store(-1)
c.lastWriteNS.Store(0)
return nil
return runID, nil
}
// IsLive reports whether the PTY is currently attached and running.
@@ -222,6 +216,21 @@ func (c *Child) Emulator() *vt.GhosttyEmulator {
return c.em
}
func (c *Child) ptyForRun(runID uint64) *pkgpty.PTY {
c.ptyMu.RLock()
defer c.ptyMu.RUnlock()
if c.runID != runID {
return nil
}
return c.pty
}
func (c *Child) isCurrentRun(runID uint64) bool {
c.ptyMu.RLock()
defer c.ptyMu.RUnlock()
return c.runID == runID
}
// DisplayName is the rename_process-aware accessor for Name. Callers
// that read Name directly skip the lock; the field is still safe to
// read because Go strings are immutable, but DisplayName signals intent.
@@ -425,6 +434,25 @@ func (c *Child) teardownPTY() {
}
}
func (c *Child) AddCleanupPath(path string) {
if path == "" {
return
}
c.cleanupMu.Lock()
c.cleanupPaths = append(c.cleanupPaths, path)
c.cleanupMu.Unlock()
}
func (c *Child) cleanupOwnedPaths() {
c.cleanupMu.Lock()
paths := c.cleanupPaths
c.cleanupPaths = nil
c.cleanupMu.Unlock()
for _, p := range paths {
_ = os.RemoveAll(p)
}
}
// InjectAsUser is the path the human takes when typing in the focused
// pane. SPEC §6: the user's first keystroke flips ownership.
func (c *Child) InjectAsUser(b []byte) error {