mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-03 12:28:10 +08:00
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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user