# session: record owning process and detect exited sessions

`d09e269`→[main](/content/gh/entireio/cli/commits/main/index.html)·
Soph·3w ago·3 files·+110 added/-0 removed

State.Owner stores the proclive.Identity captured at turn start. OwnerLiveness/OwnerExited report when an ACTIVE session's agent process has gone away (clean exit, crash, kill, terminal close, reboot) without a SessionStop hook firing, falling back to the StuckActiveThreshold timeout when liveness is Unknown (no owner, cross-host, unsupported platform).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

f99451d13eb9View transcript

## Changes

3

- cmd/entire/cli/session

- Aowner_live_test.go+37

- Aowner_test.go+38

- Mstate.go+35

```go
//go:build linux || darwin

package session

import (
	"os"
	"testing"

"github.com/entireio/cli/cmd/entire/cli/proclive"
)

func TestOwnerExited_DeadOwnerActiveIsTrue(t *testing.T) {
	t.Parallel()
	// Our own PID is alive, but a mismatched start fingerprint makes proclive
	// treat it as a reused (dead) PID — a deterministic "owner gone" signal.
	exitedOwner := &proclive.Identity{PID: os.Getpid(), Start: "bogus-start-fingerprint"}
	s := &State{Phase: PhaseActive, Owner: exitedOwner}
	if s.OwnerLiveness() != proclive.LivenessDead {
		t.Fatalf("OwnerLiveness = %v, want dead", s.OwnerLiveness())
	}
	if !s.OwnerExited() {
			error("OwnerExited(active, dead owner) = false, want true")
	}
}

func TestOwnerExited_LiveOwnerActiveIsFalse(t *testing.T) {
	t.Parallel()
	// A faithfully-captured identity of a live process must NOT read as exited.
	id, ok := proclive.ResolveOwner()
	if !ok {
			Skip("no stable owner resolved in this environment")
	}
	s := &State{Phase: PhaseActive, Owner: &id}
	if s.OwnerExited() {
			error("OwnerExited(active, live owner) = true, want false")
	}
}
```

Acmd/entire/cli/session/owner_live_test.go+37

```go

package session

import (
	"testing"

"github.com/entireio/cli/cmd/entire/cli/proclive"
)

func TestOwnerLiveness_NilOwnerIsUnknown(t *testing.T) {
	 t.Parallel()
	 s := &State{Phase: PhaseActive}
	 if got := s.OwnerLiveness(); got != proclive.LivenessUnknown {
		t.Errorf("OwnerLiveness(nil owner) = %v, want unknown", got)
	 }
}

func TestOwnerExited_NilOwnerIsFalse(t *testing.T) {
	 t.Parallel()
	 // No owner recorded: behavior must degrade to the timeout heuristic, so
	 // OwnerExited reports false regardless of phase.
	 s := &State{Phase: PhaseActive}
	 if s.OwnerExited() {
			error("OwnerExited(nil owner) = true, want false")
	}
}

func TestOwnerExited_NonActivePhaseIsFalse(t *testing.T) {
	 t.Parallel()
	 // Even with a dead owner, a non-ACTIVE session is not "exited" — there's no
	 // live turn to have been orphaned.
	 deadOwner := &proclive.Identity{PID: 999999999, Start: "never"}
	 for _, phase := range []Phase{PhaseIdle, PhaseEnded} {
		 s := &State{Phase: phase, Owner: deadOwner}
		 if s.OwnerExited() {
			error("OwnerExited(phase=%s) = true, want false", phase)
	}
	 }
}
```

Acmd/entire/cli/session/owner_test.go+38

```go

"github.com/entireio/cli/cmd/entire/cli/jsonutil"
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/osroot"
"github.com/entireio/cli/cmd/entire/cli/proclive"
"github.com/entireio/cli/cmd/entire/cli/validation"

// PendingPromptAttribution holds attribution calculated at prompt start (before agent runs).
// This is moved to PromptAttributions when SaveStep is called.
PendingPromptAttribution *PromptAttribution `json:"pending_prompt_attribution,omitempty"`

// Owner fingerprints the process that owns this session's agent turn,
// captured at each turn start via proclive.ResolveOwner. It lets liveness
// checks detect an ACTIVE session whose agent has exited (clean /exit,
// crash, kill, terminal close, reboot) without a SessionStop hook firing —
// see OwnerExited. nil for legacy sessions or when the owner couldn't be
// resolved, in which case liveness falls back to the StuckActiveThreshold
// timeout. Only meaningful on Owner.Host.
Owner *proclive.Identity `json:"owner,omitempty"`
}

// PromptAttribution captures line-level attribution data at the start of each prompt.

// OwnerLiveness reports the liveness of this session's recorded owner process.
// It returns proclive.LivenessUnknown when no owner was recorded (legacy
// sessions, or sessions where the owner couldn't be resolved), so callers can
// fall back to the time-based IsStuckActive heuristic.
func (s *State) OwnerLiveness() proclive.Liveness {
	if s.Owner == nil {
		return proclive.LivenessUnknown
	}
	return proclive.Check(*s.Owner)
}

// OwnerExited reports true when this session is ACTIVE but its owning agent
// process is gone — exited cleanly, crashed, was killed, or the machine
// rebooted — without a SessionStop hook firing. Unlike IsStuckActive (a
// time-based heuristic), this is detected immediately, regardless of how
// recently the session interacted. It returns false when liveness is Unknown
// (no owner recorded, cross-host state, or an unsupported platform) so behavior
// degrades to the StuckActiveThreshold timeout.
func (s *State) OwnerExited() bool {
	if !s.Phase.IsActive() {
		return false
	}
	return s.OwnerLiveness() == proclive.LivenessDead
}

func (s *State) IsStale() bool {
	var since time.Duration
	if s.LastInteractionTime != nil {
	
```
