From 8fa06ec55b752b60180c273362f7d848ff255191 Mon Sep 17 00:00:00 2001 From: codebanditssss Date: Sat, 13 Jun 2026 01:48:11 +0530 Subject: [PATCH] fix(session): stamp session timestamps in UTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two clocks defaulted to local time.Now while the rest of the codebase writes UTC, so ao session get showed created and updated in different timezones: - session manager clock → spawn-stamped CreatedAt/UpdatedAt - lifecycle manager clock → activity-driven LastActivityAt/UpdatedAt A real spawn made this visible: createdAt came back UTC but updatedAt/ lastActivityAt were local once the agent reported activity. Default both clocks to UTC. Closes #214 --- backend/internal/lifecycle/manager.go | 6 +++++- backend/internal/lifecycle/manager_test.go | 15 +++++++++++++++ backend/internal/session_manager/manager.go | 5 ++++- .../internal/session_manager/manager_test.go | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) 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")}