fix(core): handle reset_on_idle_mins in v1.3.3-beta.4 (#1238)

In v1.3.3-beta.4, `reset_on_idle_mins` could be configured correctly but
the idle session rotation never fired. The root cause is that
`Session.Unlock()` bumps `UpdatedAt` on every heartbeat execution and
unsolicited agent response, so the idle check in
`maybeAutoResetSessionOnIdle` always saw a fresh timestamp and never
rotated the session.

Add `Session.LastUserActivity`, a separate timestamp that is only set
via `TouchUserActivity()` when the engine processes a real incoming user
message (after the idle-reset check). The idle check now prefers
`LastUserActivity` and falls back to `UpdatedAt` for sessions created
before this field was introduced. The previous behaviour is preserved
on disk because the new field is `omitempty`.

A new regression test (`TestHandleMessage_AutoResetOnIdle_FiresWhenHeartbeatBumpedUpdatedAt`)
stubs a stale `LastUserActivity`, simulates a heartbeat by calling
`Unlock()`, and verifies that the next user message still triggers
idle rotation.

Fixes #1221

Co-authored-by: Claude <claude@anthropic.com>
This commit is contained in:
cg33
2026-06-07 15:20:40 +08:00
committed by GitHub
parent 035ef11fc9
commit e5128c3240

View File

@@ -3212,6 +3212,7 @@ func TestHandleMessage_AutoResetOnIdle_RotatesToNewSession(t *testing.T) {
old.SetAgentSessionID("old-session", "stub")
staleAt := time.Now().Add(-2 * time.Hour)
old.mu.Lock()
old.LastUserActivity = staleAt
old.UpdatedAt = staleAt
old.mu.Unlock()
@@ -3287,6 +3288,7 @@ func TestHandleMessage_AutoResetOnIdle_DoesNotRotateFreshSession(t *testing.T) {
session.SetAgentSessionID("existing-session", "stub")
recentAt := time.Now().Add(-5 * time.Minute)
session.mu.Lock()
session.LastUserActivity = recentAt
session.UpdatedAt = recentAt
session.mu.Unlock()
@@ -3325,6 +3327,71 @@ func TestHandleMessage_AutoResetOnIdle_DoesNotRotateFreshSession(t *testing.T) {
}
}
func TestHandleMessage_AutoResetOnIdle_FiresWhenHeartbeatBumpedUpdatedAt(t *testing.T) {
p := &stubPlatformEngine{n: "test"}
agentSession := newResultAgentSession("fresh reply")
agent := &resultAgent{session: agentSession}
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
e.SetResetOnIdle(60 * time.Minute)
key := "test:user1"
old := e.sessions.GetOrCreateActive(key)
old.AddHistory("user", "stale context")
old.SetAgentSessionID("old-session", "stub")
// Last user message was a long time ago — well past the idle threshold.
staleAt := time.Now().Add(-2 * time.Hour)
old.mu.Lock()
old.LastUserActivity = staleAt
old.mu.Unlock()
// Simulate a heartbeat (or unsolicited agent response) finishing right
// before this test's user message: Unlock() bumps UpdatedAt to now, but
// LastUserActivity is intentionally NOT touched by those code paths.
old.Unlock()
if !old.GetUpdatedAt().After(staleAt) {
t.Fatalf("expected Unlock to bump UpdatedAt, got %v vs %v", old.GetUpdatedAt(), staleAt)
}
if !old.GetLastUserActivity().Equal(staleAt) {
t.Fatalf("expected LastUserActivity to remain at %v, got %v", staleAt, old.GetLastUserActivity())
}
msg := &Message{
SessionKey: key,
Platform: "test",
UserID: "u1",
UserName: "user",
Content: "hello after idle",
ReplyCtx: "ctx",
}
e.handleMessage(p, msg)
deadline := time.After(2 * time.Second)
for {
active := e.sessions.GetOrCreateActive(key)
sent := p.getSent()
if active.ID != old.ID && len(active.GetHistory(0)) >= 2 && len(sent) >= 2 {
break
}
select {
case <-deadline:
t.Fatalf("timed out waiting for idle auto-reset despite heartbeat-bumped UpdatedAt, sent=%v active=%s old=%s", sent, active.ID, old.ID)
default:
time.Sleep(10 * time.Millisecond)
}
}
active := e.sessions.GetOrCreateActive(key)
if active.ID == old.ID {
t.Fatal("expected a new active session after idle auto-reset")
}
sent := p.getSent()
if !strings.Contains(sent[0], "Session auto-reset") {
t.Fatalf("first reply = %q, want auto-reset notice", sent[0])
}
}
func TestHandleMessage_AutoResetOnIdle_DoesNotTriggerForSlashCommand(t *testing.T) {
p := &stubPlatformEngine{n: "test"}
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
@@ -3336,6 +3403,7 @@ func TestHandleMessage_AutoResetOnIdle_DoesNotTriggerForSlashCommand(t *testing.
session.SetAgentSessionID("old-session", "stub")
staleAt := time.Now().Add(-2 * time.Hour)
session.mu.Lock()
session.LastUserActivity = staleAt
session.UpdatedAt = staleAt
session.mu.Unlock()