diff --git a/backend/internal/lifecycle/manager.go b/backend/internal/lifecycle/manager.go index 925af553..360f17b3 100644 --- a/backend/internal/lifecycle/manager.go +++ b/backend/internal/lifecycle/manager.go @@ -37,7 +37,11 @@ type Manager struct { // New builds a Lifecycle Manager over the session store it writes and the messenger it uses for agent nudges. func New(store sessionStore, messenger ports.AgentMessenger) *Manager { - return &Manager{store: store, messenger: messenger, window: defaultRecentActivityWindow, clock: time.Now, react: newReactionState()} + // UTC so activity-driven LastActivityAt/UpdatedAt match spawn-stamped + // timestamps (the session manager clock is UTC too); a local clock here left + // `ao session get` showing created in UTC but updated in local time. + clock := func() time.Time { return time.Now().UTC() } + return &Manager{store: store, messenger: messenger, window: defaultRecentActivityWindow, clock: clock, react: newReactionState()} } func (m *Manager) mutate(ctx context.Context, id domain.SessionID, fn func(domain.SessionRecord, time.Time) (domain.SessionRecord, bool)) error { diff --git a/backend/internal/lifecycle/manager_test.go b/backend/internal/lifecycle/manager_test.go index e739fa01..4190de5e 100644 --- a/backend/internal/lifecycle/manager_test.go +++ b/backend/internal/lifecycle/manager_test.go @@ -138,6 +138,21 @@ func TestMarkSpawnedStoresRuntimeMetadata(t *testing.T) { } } +// TestMarkSpawned_StampsUTCActivity locks the lifecycle clock to UTC so +// activity-driven timestamps match the session manager's spawn timestamps. A +// local clock here left `ao session get` showing created in UTC but updated in +// local time. +func TestMarkSpawned_StampsUTCActivity(t *testing.T) { + m, st, _ := newManager() + st.sessions["mer-1"] = domain.SessionRecord{ID: "mer-1", ProjectID: "mer", IsTerminated: true} + if err := m.MarkSpawned(ctx, "mer-1", domain.SessionMetadata{RuntimeHandleID: "h1"}); err != nil { + t.Fatal(err) + } + if loc := st.sessions["mer-1"].Activity.LastActivityAt.Location(); loc != time.UTC { + t.Fatalf("LastActivityAt location = %v, want UTC", loc) + } +} + func TestPRObservation_CIFailingNudgesAgentWithLogs(t *testing.T) { m, st, msg := newManager() st.sessions["mer-1"] = working("mer-1") diff --git a/backend/internal/session_manager/manager.go b/backend/internal/session_manager/manager.go index 9a563e96..49bbcaea 100644 --- a/backend/internal/session_manager/manager.go +++ b/backend/internal/session_manager/manager.go @@ -136,7 +136,10 @@ func New(d Deps) *Manager { logger: d.Logger, } if m.clock == nil { - m.clock = time.Now + // UTC so spawn-stamped CreatedAt/UpdatedAt match every other session + // write (rename, activity) — all of which use time.Now().UTC(). A local + // default produced mixed-timezone timestamps in `ao session get`. + m.clock = func() time.Time { return time.Now().UTC() } } if m.lookPath == nil { m.lookPath = exec.LookPath diff --git a/backend/internal/session_manager/manager_test.go b/backend/internal/session_manager/manager_test.go index cc7832e2..a877469d 100644 --- a/backend/internal/session_manager/manager_test.go +++ b/backend/internal/session_manager/manager_test.go @@ -293,6 +293,25 @@ func TestSpawn_AssignsIDAndGoesIdle(t *testing.T) { t.Fatal("handle not folded") } } + +// TestSpawn_StampsUTCTimestamps locks the default clock to UTC so spawn-stamped +// CreatedAt/UpdatedAt match every other session write (rename, activity), which +// all use time.Now().UTC(). A local default produced mixed-timezone timestamps +// in `ao session get` (created in local time, updated in UTC). +func TestSpawn_StampsUTCTimestamps(t *testing.T) { + m, st, _, _ := newManager() + if _, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer", Kind: domain.KindWorker}); err != nil { + t.Fatal(err) + } + rec := st.sessions["mer-1"] + if loc := rec.CreatedAt.Location(); loc != time.UTC { + t.Fatalf("CreatedAt location = %v, want UTC", loc) + } + if loc := rec.UpdatedAt.Location(); loc != time.UTC { + t.Fatalf("UpdatedAt location = %v, want UTC", loc) + } +} + func TestSpawn_RollsBackOnRuntimeFailure(t *testing.T) { m, st, _, ws := newManager() m.runtime = &fakeRuntime{createErr: errors.New("boom")}