Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 25 additions & 25 deletions cmd/entire/cli/integration_test/last_interaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"time"
)

// TestLastInteractionAt_SetOnFirstPrompt verifies that LastInteractionAt is set
// TestLastInteractionTime_SetOnFirstPrompt verifies that LastInteractionTime is set
// when a session is first initialized via UserPromptSubmit.
func TestLastInteractionAt_SetOnFirstPrompt(t *testing.T) {
func TestLastInteractionTime_SetOnFirstPrompt(t *testing.T) {
t.Parallel()
RunForAllStrategiesWithRepoEnv(t, func(t *testing.T, env *TestEnv, _ string) {
session := env.NewSession()
Expand All @@ -27,19 +27,19 @@ func TestLastInteractionAt_SetOnFirstPrompt(t *testing.T) {
t.Fatal("session state should exist after UserPromptSubmit")
}

if state.LastInteractionAt == nil {
t.Fatal("LastInteractionAt should be set after first prompt")
if state.LastInteractionTime == nil {
t.Fatal("LastInteractionTime should be set after first prompt")
}
if state.LastInteractionAt.Before(beforePrompt) {
t.Errorf("LastInteractionAt %v should be after test start %v",
*state.LastInteractionAt, beforePrompt)
if state.LastInteractionTime.Before(beforePrompt) {
t.Errorf("LastInteractionTime %v should be after test start %v",
*state.LastInteractionTime, beforePrompt)
}
})
}

// TestLastInteractionAt_UpdatedOnSubsequentPrompts verifies that LastInteractionAt
// TestLastInteractionTime_UpdatedOnSubsequentPrompts verifies that LastInteractionTime
// is updated on each subsequent UserPromptSubmit call.
func TestLastInteractionAt_UpdatedOnSubsequentPrompts(t *testing.T) {
func TestLastInteractionTime_UpdatedOnSubsequentPrompts(t *testing.T) {
t.Parallel()
RunForAllStrategiesWithRepoEnv(t, func(t *testing.T, env *TestEnv, _ string) {
session := env.NewSession()
Expand All @@ -53,10 +53,10 @@ func TestLastInteractionAt_UpdatedOnSubsequentPrompts(t *testing.T) {
if err != nil {
t.Fatalf("GetSessionState after first prompt failed: %v", err)
}
if state1.LastInteractionAt == nil {
t.Fatal("LastInteractionAt should be set after first prompt")
if state1.LastInteractionTime == nil {
t.Fatal("LastInteractionTime should be set after first prompt")
}
firstInteraction := *state1.LastInteractionAt
firstInteraction := *state1.LastInteractionTime

// Small delay to ensure timestamps differ
time.Sleep(10 * time.Millisecond)
Expand All @@ -70,20 +70,20 @@ func TestLastInteractionAt_UpdatedOnSubsequentPrompts(t *testing.T) {
if err != nil {
t.Fatalf("GetSessionState after second prompt failed: %v", err)
}
if state2.LastInteractionAt == nil {
t.Fatal("LastInteractionAt should be set after second prompt")
if state2.LastInteractionTime == nil {
t.Fatal("LastInteractionTime should be set after second prompt")
}

if !state2.LastInteractionAt.After(firstInteraction) {
t.Errorf("LastInteractionAt should be updated: first=%v, second=%v",
firstInteraction, *state2.LastInteractionAt)
if !state2.LastInteractionTime.After(firstInteraction) {
t.Errorf("LastInteractionTime should be updated: first=%v, second=%v",
firstInteraction, *state2.LastInteractionTime)
}
})
}

// TestLastInteractionAt_PreservedAcrossCheckpoints verifies that LastInteractionAt
// TestLastInteractionTime_PreservedAcrossCheckpoints verifies that LastInteractionTime
// survives a full checkpoint cycle (prompt → stop → prompt).
func TestLastInteractionAt_PreservedAcrossCheckpoints(t *testing.T) {
func TestLastInteractionTime_PreservedAcrossCheckpoints(t *testing.T) {
t.Parallel()
RunForAllStrategiesWithRepoEnv(t, func(t *testing.T, env *TestEnv, _ string) {
session := env.NewSession()
Expand Down Expand Up @@ -112,14 +112,14 @@ func TestLastInteractionAt_PreservedAcrossCheckpoints(t *testing.T) {
if err != nil {
t.Fatalf("GetSessionState failed: %v", err)
}
if state.LastInteractionAt == nil {
t.Fatal("LastInteractionAt should be set after second prompt")
if state.LastInteractionTime == nil {
t.Fatal("LastInteractionTime should be set after second prompt")
}

// LastInteractionAt should be after StartedAt (second prompt is later)
if !state.LastInteractionAt.After(state.StartedAt) {
t.Errorf("LastInteractionAt %v should be after StartedAt %v",
*state.LastInteractionAt, state.StartedAt)
// LastInteractionTime should be after StartedAt (second prompt is later)
if !state.LastInteractionTime.After(state.StartedAt) {
t.Errorf("LastInteractionTime %v should be after StartedAt %v",
*state.LastInteractionTime, state.StartedAt)
}
})
}
5 changes: 3 additions & 2 deletions cmd/entire/cli/session/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ type State struct {
// nil means the session is still active or was not cleanly closed.
EndedAt *time.Time `json:"ended_at,omitempty"`

// LastInteractionAt is the last time a user prompt was submitted for this session.
LastInteractionAt *time.Time `json:"last_interaction_at,omitempty"`
// LastInteractionTime is updated on every hook invocation.
// Used for stale session detection in "entire sessions fix".
LastInteractionTime *time.Time `json:"last_interaction_time,omitempty"`

// CheckpointCount is the number of checkpoints created in this session
CheckpointCount int `json:"checkpoint_count"`
Expand Down
6 changes: 3 additions & 3 deletions cmd/entire/cli/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,10 @@ func writeActiveSessions(w io.Writer) {

age := "started " + timeAgo(st.StartedAt)

// Show "active X ago" when LastInteractionAt differs meaningfully from StartedAt
// Show "active X ago" when LastInteractionTime differs meaningfully from StartedAt
activeStr := ""
if st.LastInteractionAt != nil && st.LastInteractionAt.Sub(st.StartedAt) > time.Minute {
activeStr = ", active " + timeAgo(*st.LastInteractionAt)
if st.LastInteractionTime != nil && st.LastInteractionTime.Sub(st.StartedAt) > time.Minute {
activeStr = ", active " + timeAgo(*st.LastInteractionTime)
}

fmt.Fprintf(w, " [%s] %-9s %s%s\n",
Expand Down
36 changes: 18 additions & 18 deletions cmd/entire/cli/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,12 @@ func TestWriteActiveSessions(t *testing.T) {
// Create active sessions
states := []*session.State{
{
SessionID: "abc-1234-session",
WorktreePath: "/Users/test/repo",
StartedAt: now.Add(-2 * time.Hour),
LastInteractionAt: &recentInteraction,
FirstPrompt: "Fix auth bug in login flow",
AgentType: agent.AgentType("Claude Code"),
SessionID: "abc-1234-session",
WorktreePath: "/Users/test/repo",
StartedAt: now.Add(-2 * time.Hour),
LastInteractionTime: &recentInteraction,
FirstPrompt: "Fix auth bug in login flow",
AgentType: agent.AgentType("Claude Code"),
},
{
SessionID: "def-5678-session",
Expand Down Expand Up @@ -296,17 +296,17 @@ func TestWriteActiveSessions(t *testing.T) {
}
}

// Should show "active X ago" for session with LastInteractionAt that differs from StartedAt
// Should show "active X ago" for session with LastInteractionTime that differs from StartedAt
if !strings.Contains(output, "active 5m ago") {
t.Errorf("Expected 'active 5m ago' for session with LastInteractionAt, got: %s", output)
t.Errorf("Expected 'active 5m ago' for session with LastInteractionTime, got: %s", output)
}

// Session started 15m ago with no LastInteractionAt should NOT show "active" text
// Session started 15m ago with no LastInteractionTime should NOT show "active" text
// Find the Cursor session line and verify no "active" in it
for _, line := range lines {
if strings.Contains(line, "[Cursor]") {
if strings.Contains(line, "active") {
t.Errorf("Session without LastInteractionAt should not show 'active', got: %s", line)
t.Errorf("Session without LastInteractionTime should not show 'active', got: %s", line)
}
}
}
Expand All @@ -321,17 +321,17 @@ func TestWriteActiveSessions_ActiveTimeOmittedWhenClose(t *testing.T) {
}

now := time.Now()
// LastInteractionAt is only 30 seconds after StartedAt — should be omitted
// LastInteractionTime is only 30 seconds after StartedAt — should be omitted
startedAt := now.Add(-10 * time.Minute)
lastInteraction := startedAt.Add(30 * time.Second)

state := &session.State{
SessionID: "close-time-session",
WorktreePath: "/Users/test/repo",
StartedAt: startedAt,
LastInteractionAt: &lastInteraction,
FirstPrompt: "test prompt",
AgentType: agent.AgentType("Claude Code"),
SessionID: "close-time-session",
WorktreePath: "/Users/test/repo",
StartedAt: startedAt,
LastInteractionTime: &lastInteraction,
FirstPrompt: "test prompt",
AgentType: agent.AgentType("Claude Code"),
}

if err := store.Save(context.Background(), state); err != nil {
Expand All @@ -343,7 +343,7 @@ func TestWriteActiveSessions_ActiveTimeOmittedWhenClose(t *testing.T) {

output := buf.String()
if strings.Contains(output, "active") {
t.Errorf("Expected no 'active' when LastInteractionAt is close to StartedAt, got: %s", output)
t.Errorf("Expected no 'active' when LastInteractionTime is close to StartedAt, got: %s", output)
}
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/entire/cli/strategy/auto_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,7 @@ func (s *AutoCommitStrategy) InitializeSession(sessionID string, agentType agent
if existing != nil {
// Session already initialized — update last interaction time on every prompt submit
now := time.Now()
existing.LastInteractionAt = &now
existing.LastInteractionTime = &now

// Backfill FirstPrompt if empty (for sessions
// created before the first_prompt field was added, or resumed sessions)
Expand All @@ -946,7 +946,7 @@ func (s *AutoCommitStrategy) InitializeSession(sessionID string, agentType agent
SessionID: sessionID,
BaseCommit: baseCommit,
StartedAt: now,
LastInteractionAt: &now,
LastInteractionTime: &now,
CheckpointCount: 0,
CondensedTranscriptLines: 0, // Start from beginning of transcript
FilesTouched: []string{},
Expand Down
4 changes: 2 additions & 2 deletions cmd/entire/cli/strategy/manual_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func sessionStateToStrategy(state *session.State) *SessionState {
WorktreeID: state.WorktreeID,
StartedAt: state.StartedAt,
EndedAt: state.EndedAt,
LastInteractionAt: state.LastInteractionAt,
LastInteractionTime: state.LastInteractionTime,
CheckpointCount: state.CheckpointCount,
CondensedTranscriptLines: state.CondensedTranscriptLines,
UntrackedFilesAtStart: state.UntrackedFilesAtStart,
Expand Down Expand Up @@ -117,7 +117,7 @@ func sessionStateFromStrategy(state *SessionState) *session.State {
WorktreeID: state.WorktreeID,
StartedAt: state.StartedAt,
EndedAt: state.EndedAt,
LastInteractionAt: state.LastInteractionAt,
LastInteractionTime: state.LastInteractionTime,
CheckpointCount: state.CheckpointCount,
CondensedTranscriptLines: state.CondensedTranscriptLines,
UntrackedFilesAtStart: state.UntrackedFilesAtStart,
Expand Down
2 changes: 1 addition & 1 deletion cmd/entire/cli/strategy/manual_commit_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@ func (s *ManualCommitStrategy) InitializeSession(sessionID string, agentType age

// Update last interaction timestamp on every prompt submit
now := time.Now()
state.LastInteractionAt = &now
state.LastInteractionTime = &now

// Backfill AgentType if empty or set to the generic default "Agent"
if !isSpecificAgentType(state.AgentType) && agentType != "" {
Expand Down
2 changes: 1 addition & 1 deletion cmd/entire/cli/strategy/manual_commit_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func (s *ManualCommitStrategy) initializeSession(repo *git.Repository, sessionID
WorktreePath: worktreePath,
WorktreeID: worktreeID,
StartedAt: now,
LastInteractionAt: &now,
LastInteractionTime: &now,
CheckpointCount: 0,
UntrackedFilesAtStart: untrackedFiles,
AgentType: agentType,
Expand Down
4 changes: 2 additions & 2 deletions cmd/entire/cli/strategy/manual_commit_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type SessionState struct {
WorktreePath string `json:"worktree_path,omitempty"` // Absolute path to the worktree root
WorktreeID string `json:"worktree_id,omitempty"` // Internal git worktree identifier (empty for main worktree)
StartedAt time.Time `json:"started_at"`
EndedAt *time.Time `json:"ended_at,omitempty"` // When the session was explicitly closed (nil = active or unclean exit)
LastInteractionAt *time.Time `json:"last_interaction_at,omitempty"` // Last user prompt submit time
EndedAt *time.Time `json:"ended_at,omitempty"` // When the session was explicitly closed (nil = active or unclean exit)
LastInteractionTime *time.Time `json:"last_interaction_time,omitempty"` // Updated on every hook invocation
CheckpointCount int `json:"checkpoint_count"`
CondensedTranscriptLines int `json:"condensed_transcript_lines,omitempty"` // Lines already included in previous condensation
UntrackedFilesAtStart []string `json:"untracked_files_at_start,omitempty"` // Files that existed at session start (to preserve during rewind)
Expand Down
44 changes: 22 additions & 22 deletions cmd/entire/cli/strategy/session_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ func TestLoadSessionState_WithEndedAt(t *testing.T) {
}
}

// TestLoadSessionState_WithLastInteractionAt tests that LastInteractionAt serializes/deserializes correctly.
func TestLoadSessionState_WithLastInteractionAt(t *testing.T) {
// TestLoadSessionState_WithLastInteractionTime tests that LastInteractionTime serializes/deserializes correctly.
func TestLoadSessionState_WithLastInteractionTime(t *testing.T) {
dir := t.TempDir()
_, err := git.PlainInit(dir, false)
if err != nil {
Expand All @@ -141,14 +141,14 @@ func TestLoadSessionState_WithLastInteractionAt(t *testing.T) {

t.Chdir(dir)

// Test with LastInteractionAt set
// Test with LastInteractionTime set
lastInteraction := time.Now().Add(-5 * time.Minute)
state := &SessionState{
SessionID: "test-session-interaction",
BaseCommit: "abc123def456",
StartedAt: time.Now().Add(-2 * time.Hour),
LastInteractionAt: &lastInteraction,
CheckpointCount: 3,
SessionID: "test-session-interaction",
BaseCommit: "abc123def456",
StartedAt: time.Now().Add(-2 * time.Hour),
LastInteractionTime: &lastInteraction,
CheckpointCount: 3,
}

err = SaveSessionState(state)
Expand All @@ -164,21 +164,21 @@ func TestLoadSessionState_WithLastInteractionAt(t *testing.T) {
t.Fatal("LoadSessionState() returned nil")
}

// Verify LastInteractionAt was preserved
if loaded.LastInteractionAt == nil {
t.Fatal("LastInteractionAt was nil after load, expected non-nil")
// Verify LastInteractionTime was preserved
if loaded.LastInteractionTime == nil {
t.Fatal("LastInteractionTime was nil after load, expected non-nil")
}
if !loaded.LastInteractionAt.Equal(lastInteraction) {
t.Errorf("LastInteractionAt = %v, want %v", *loaded.LastInteractionAt, lastInteraction)
if !loaded.LastInteractionTime.Equal(lastInteraction) {
t.Errorf("LastInteractionTime = %v, want %v", *loaded.LastInteractionTime, lastInteraction)
}

// Test with LastInteractionAt nil (old session without this field)
// Test with LastInteractionTime nil (old session without this field)
stateOld := &SessionState{
SessionID: "test-session-no-interaction",
BaseCommit: "xyz789",
StartedAt: time.Now(),
LastInteractionAt: nil,
CheckpointCount: 1,
SessionID: "test-session-no-interaction",
BaseCommit: "xyz789",
StartedAt: time.Now(),
LastInteractionTime: nil,
CheckpointCount: 1,
}

err = SaveSessionState(stateOld)
Expand All @@ -194,9 +194,9 @@ func TestLoadSessionState_WithLastInteractionAt(t *testing.T) {
t.Fatal("LoadSessionState() returned nil")
}

// Verify LastInteractionAt remains nil
if loadedOld.LastInteractionAt != nil {
t.Errorf("LastInteractionAt = %v, want nil for old session", *loadedOld.LastInteractionAt)
// Verify LastInteractionTime remains nil
if loadedOld.LastInteractionTime != nil {
t.Errorf("LastInteractionTime = %v, want nil for old session", *loadedOld.LastInteractionTime)
}
}

Expand Down
Loading