diff --git a/cmd/entire/cli/session/phase.go b/cmd/entire/cli/session/phase.go index 599ef9b9e..2f7c931ae 100644 --- a/cmd/entire/cli/session/phase.go +++ b/cmd/entire/cli/session/phase.go @@ -168,7 +168,7 @@ func transitionFromIdle(event Event, ctx TransitionContext) TransitionResult { } return TransitionResult{ NewPhase: PhaseIdle, - Actions: []Action{ActionCondense, ActionUpdateLastInteraction}, + Actions: []Action{ActionCondense}, } case EventSessionStart: // Already condensable, no-op. @@ -208,7 +208,7 @@ func transitionFromActive(event Event, ctx TransitionContext) TransitionResult { } return TransitionResult{ NewPhase: PhaseActive, - Actions: []Action{ActionCondense, ActionUpdateLastInteraction}, + Actions: []Action{ActionCondense}, } case EventSessionStart: return TransitionResult{ @@ -249,12 +249,12 @@ func transitionFromEnded(event Event, ctx TransitionContext) TransitionResult { if ctx.HasFilesTouched { return TransitionResult{ NewPhase: PhaseEnded, - Actions: []Action{ActionCondenseIfFilesTouched, ActionUpdateLastInteraction}, + Actions: []Action{ActionCondenseIfFilesTouched}, } } return TransitionResult{ NewPhase: PhaseEnded, - Actions: []Action{ActionDiscardIfNoFiles, ActionUpdateLastInteraction}, + Actions: []Action{ActionDiscardIfNoFiles}, } case EventSessionStart: return TransitionResult{ diff --git a/cmd/entire/cli/session/phase_test.go b/cmd/entire/cli/session/phase_test.go index 6e4aae20d..95cb001bd 100644 --- a/cmd/entire/cli/session/phase_test.go +++ b/cmd/entire/cli/session/phase_test.go @@ -140,7 +140,7 @@ func TestTransitionFromIdle(t *testing.T) { current: PhaseIdle, event: EventGitCommit, wantPhase: PhaseIdle, - wantActions: []Action{ActionCondense, ActionUpdateLastInteraction}, + wantActions: []Action{ActionCondense}, }, { name: "GitCommit_rebase_skips_everything", @@ -196,7 +196,7 @@ func TestTransitionFromActive(t *testing.T) { current: PhaseActive, event: EventGitCommit, wantPhase: PhaseActive, - wantActions: []Action{ActionCondense, ActionUpdateLastInteraction}, + wantActions: []Action{ActionCondense}, }, { name: "GitCommit_rebase_skips_everything", @@ -239,14 +239,14 @@ func TestTransitionFromEnded(t *testing.T) { event: EventGitCommit, ctx: TransitionContext{HasFilesTouched: true}, wantPhase: PhaseEnded, - wantActions: []Action{ActionCondenseIfFilesTouched, ActionUpdateLastInteraction}, + wantActions: []Action{ActionCondenseIfFilesTouched}, }, { name: "GitCommit_without_files_discards", current: PhaseEnded, event: EventGitCommit, wantPhase: PhaseEnded, - wantActions: []Action{ActionDiscardIfNoFiles, ActionUpdateLastInteraction}, + wantActions: []Action{ActionDiscardIfNoFiles}, }, { name: "GitCommit_rebase_skips_everything", @@ -295,7 +295,7 @@ func TestTransitionBackwardCompat(t *testing.T) { current: Phase(""), event: EventGitCommit, wantPhase: PhaseIdle, - wantActions: []Action{ActionCondense, ActionUpdateLastInteraction}, + wantActions: []Action{ActionCondense}, }, { name: "empty_phase_SessionStop_treated_as_IDLE", @@ -441,7 +441,7 @@ func TestApplyTransition_CallsHandlerForCondense(t *testing.T) { handler := &mockActionHandler{} result := TransitionResult{ NewPhase: PhaseIdle, - Actions: []Action{ActionCondense, ActionUpdateLastInteraction}, + Actions: []Action{ActionCondense}, } err := ApplyTransition(context.Background(), state, result, handler) @@ -449,7 +449,6 @@ func TestApplyTransition_CallsHandlerForCondense(t *testing.T) { require.NoError(t, err) assert.True(t, handler.condenseCalled) assert.Equal(t, PhaseIdle, state.Phase) - require.NotNil(t, state.LastInteractionTime) } func TestApplyTransition_CallsHandlerForCondenseIfFilesTouched(t *testing.T) { @@ -459,7 +458,7 @@ func TestApplyTransition_CallsHandlerForCondenseIfFilesTouched(t *testing.T) { handler := &mockActionHandler{} result := TransitionResult{ NewPhase: PhaseEnded, - Actions: []Action{ActionCondenseIfFilesTouched, ActionUpdateLastInteraction}, + Actions: []Action{ActionCondenseIfFilesTouched}, } err := ApplyTransition(context.Background(), state, result, handler) @@ -475,7 +474,7 @@ func TestApplyTransition_CallsHandlerForDiscardIfNoFiles(t *testing.T) { handler := &mockActionHandler{} result := TransitionResult{ NewPhase: PhaseEnded, - Actions: []Action{ActionDiscardIfNoFiles, ActionUpdateLastInteraction}, + Actions: []Action{ActionDiscardIfNoFiles}, } err := ApplyTransition(context.Background(), state, result, handler) @@ -552,25 +551,22 @@ func TestApplyTransition_ClearsEndedAt(t *testing.T) { } } -func TestApplyTransition_ReturnsHandlerError_ButRunsCommonActions(t *testing.T) { +func TestApplyTransition_ReturnsHandlerError_ButSetsPhase(t *testing.T) { t.Parallel() state := &State{Phase: PhaseActive} handler := &mockActionHandler{returnErr: errors.New("condense failed")} - // Synthetic transition with [Condense, UpdateLastInteraction] actions. result := TransitionResult{ NewPhase: PhaseIdle, - Actions: []Action{ActionCondense, ActionUpdateLastInteraction}, + Actions: []Action{ActionCondense}, } err := ApplyTransition(context.Background(), state, result, handler) require.Error(t, err) assert.Contains(t, err.Error(), "condense failed") + // Phase must still be set even though handler failed. assert.Equal(t, PhaseIdle, state.Phase) - // Common action must still run even though handler failed. - require.NotNil(t, state.LastInteractionTime, - "UpdateLastInteraction must run despite earlier handler error") } func TestApplyTransition_StopsOnFirstHandlerError(t *testing.T) { @@ -590,6 +586,24 @@ func TestApplyTransition_StopsOnFirstHandlerError(t *testing.T) { assert.False(t, handler.warnStaleSessionCalled, "should stop on first error") } +func TestApplyTransition_UpdateLastInteractionRunsDespiteHandlerError(t *testing.T) { + t.Parallel() + + state := &State{Phase: PhaseEnded} + handler := &mockActionHandler{returnErr: errors.New("condense failed")} + result := TransitionResult{ + NewPhase: PhaseEnded, + Actions: []Action{ActionCondenseIfFilesTouched, ActionUpdateLastInteraction}, + } + + err := ApplyTransition(context.Background(), state, result, handler) + + require.Error(t, err) + assert.Contains(t, err.Error(), "condense failed") + assert.True(t, handler.condenseIfFilesTouchedCalled) + require.NotNil(t, state.LastInteractionTime, "UpdateLastInteraction must run despite earlier handler error") +} + func TestApplyTransition_ClearEndedAtRunsDespiteHandlerError(t *testing.T) { t.Parallel() diff --git a/cmd/entire/cli/session/state.go b/cmd/entire/cli/session/state.go index 1bfdfeadf..bed1f9e1e 100644 --- a/cmd/entire/cli/session/state.go +++ b/cmd/entire/cli/session/state.go @@ -84,7 +84,8 @@ type State struct { // - Cleared when session is reset (ResetSession deletes the state file entirely) TurnCheckpointIDs []string `json:"turn_checkpoint_ids,omitempty"` - // LastInteractionTime is updated on every hook invocation. + // LastInteractionTime is updated on agent-interaction events (TurnStart, + // TurnEnd, SessionStop, Compaction) but NOT on git commit hooks. // Used for stale session detection in "entire doctor". LastInteractionTime *time.Time `json:"last_interaction_time,omitempty"`