From 388d601e8ae73e57493483031ff8ca84193d52cf Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 24 Apr 2026 03:09:12 +0530 Subject: [PATCH 1/4] feat(ui): sort sessions by recent activity Signed-off-by: Asish Kumar --- go/core/internal/database/client_postgres.go | 50 +++++--- go/core/internal/database/client_test.go | 119 ++++++++++++++++++ go/core/internal/database/gen/querier.go | 2 + go/core/internal/database/gen/sessions.sql.go | 31 ++++- .../internal/database/queries/sessions.sql | 14 ++- .../sidebars/GroupedChats.stories.tsx | 18 ++- ui/src/components/sidebars/GroupedChats.tsx | 12 +- ui/src/components/sidebars/SessionGroup.tsx | 2 +- 8 files changed, 217 insertions(+), 31 deletions(-) diff --git a/go/core/internal/database/client_postgres.go b/go/core/internal/database/client_postgres.go index 15a2dbacd..e0fc77696 100644 --- a/go/core/internal/database/client_postgres.go +++ b/go/core/internal/database/client_postgres.go @@ -148,17 +148,28 @@ func (c *postgresClient) DeleteSession(ctx context.Context, sessionID, userID st // ── Events ──────────────────────────────────────────────────────────────────── func (c *postgresClient) StoreEvents(ctx context.Context, events ...*dbpkg.Event) error { - for _, e := range events { - if err := c.q.InsertEvent(ctx, dbgen.InsertEventParams{ - ID: e.ID, - UserID: e.UserID, - SessionID: strPtrIfNotEmpty(e.SessionID), - Data: e.Data, - }); err != nil { - return fmt.Errorf("failed to store event %s: %w", e.ID, err) + return c.withTx(ctx, func(q *dbgen.Queries) error { + for _, e := range events { + sessionID := strPtrIfNotEmpty(e.SessionID) + if err := q.InsertEvent(ctx, dbgen.InsertEventParams{ + ID: e.ID, + UserID: e.UserID, + SessionID: sessionID, + Data: e.Data, + }); err != nil { + return fmt.Errorf("failed to store event %s: %w", e.ID, err) + } + if sessionID != nil { + if err := q.TouchSessionForUser(ctx, dbgen.TouchSessionForUserParams{ + ID: *sessionID, + UserID: e.UserID, + }); err != nil { + return fmt.Errorf("failed to touch session %s: %w", *sessionID, err) + } + } } - } - return nil + return nil + }) } func (c *postgresClient) ListEventsForSession(ctx context.Context, sessionID, userID string, opts dbpkg.QueryOptions) ([]*dbpkg.Event, error) { @@ -202,10 +213,21 @@ func (c *postgresClient) StoreTask(ctx context.Context, task *protocol.Task) err if err != nil { return fmt.Errorf("failed to serialize task: %w", err) } - return c.q.UpsertTask(ctx, dbgen.UpsertTaskParams{ - ID: task.ID, - Data: string(data), - SessionID: strPtrIfNotEmpty(task.ContextID), + return c.withTx(ctx, func(q *dbgen.Queries) error { + sessionID := strPtrIfNotEmpty(task.ContextID) + if err := q.UpsertTask(ctx, dbgen.UpsertTaskParams{ + ID: task.ID, + Data: string(data), + SessionID: sessionID, + }); err != nil { + return err + } + if sessionID != nil { + if err := q.TouchSession(ctx, *sessionID); err != nil { + return fmt.Errorf("failed to touch session %s: %w", *sessionID, err) + } + } + return nil }) } diff --git a/go/core/internal/database/client_test.go b/go/core/internal/database/client_test.go index f7cdc4cf1..3a8af9218 100644 --- a/go/core/internal/database/client_test.go +++ b/go/core/internal/database/client_test.go @@ -13,6 +13,7 @@ import ( "github.com/pgvector/pgvector-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "trpc.group/trpc-go/trpc-a2a-go/protocol" ) // TestConcurrentAgentUpserts verifies that concurrent StoreAgent calls @@ -233,6 +234,124 @@ func TestStoreSessionIdempotence(t *testing.T) { assert.Equal(t, "Updated", *retrieved.Name, "Session should have updated name") } +func TestListSessionsOrdersByRecentActivity(t *testing.T) { + db := setupTestDB(t) + client := NewClient(db) + ctx := context.Background() + + userID := "test-user" + agentID := "test-agent" + base := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + sessions := []struct { + id string + createdAt time.Time + updatedAt time.Time + }{ + {id: "old-active", createdAt: base, updatedAt: base.Add(4 * time.Hour)}, + {id: "new-inactive", createdAt: base.Add(3 * time.Hour), updatedAt: base.Add(3 * time.Hour)}, + {id: "old-inactive", createdAt: base.Add(time.Hour), updatedAt: base.Add(2 * time.Hour)}, + } + + for _, s := range sessions { + err := client.StoreSession(ctx, &dbpkg.Session{ + ID: s.id, + UserID: userID, + AgentID: &agentID, + }) + require.NoError(t, err) + _, err = db.Exec(ctx, ` + UPDATE session + SET created_at = $1, updated_at = $2 + WHERE id = $3 AND user_id = $4 + `, s.createdAt, s.updatedAt, s.id, userID) + require.NoError(t, err) + } + + allSessions, err := client.ListSessions(ctx, userID) + require.NoError(t, err) + require.Len(t, allSessions, 3) + assert.Equal(t, []string{"old-active", "new-inactive", "old-inactive"}, []string{ + allSessions[0].ID, + allSessions[1].ID, + allSessions[2].ID, + }) + + agentSessions, err := client.ListSessionsForAgent(ctx, agentID, userID) + require.NoError(t, err) + require.Len(t, agentSessions, 3) + assert.Equal(t, []string{"old-active", "new-inactive", "old-inactive"}, []string{ + agentSessions[0].ID, + agentSessions[1].ID, + agentSessions[2].ID, + }) +} + +func TestStoreEventTouchesSessionActivity(t *testing.T) { + db := setupTestDB(t) + client := NewClient(db) + ctx := context.Background() + + userID := "test-user" + sessionID := "active-session" + oldActivity := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + + err := client.StoreSession(ctx, &dbpkg.Session{ + ID: sessionID, + UserID: userID, + }) + require.NoError(t, err) + _, err = db.Exec(ctx, ` + UPDATE session + SET updated_at = $1 + WHERE id = $2 AND user_id = $3 + `, oldActivity, sessionID, userID) + require.NoError(t, err) + + err = client.StoreEvents(ctx, &dbpkg.Event{ + ID: "event-1", + SessionID: sessionID, + UserID: userID, + Data: "{}", + }) + require.NoError(t, err) + + got, err := client.GetSession(ctx, sessionID, userID) + require.NoError(t, err) + assert.True(t, got.UpdatedAt.After(oldActivity), "session updated_at should advance after storing an event") +} + +func TestStoreTaskTouchesSessionActivity(t *testing.T) { + db := setupTestDB(t) + client := NewClient(db) + ctx := context.Background() + + userID := "test-user" + sessionID := "active-session" + oldActivity := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + + err := client.StoreSession(ctx, &dbpkg.Session{ + ID: sessionID, + UserID: userID, + }) + require.NoError(t, err) + _, err = db.Exec(ctx, ` + UPDATE session + SET updated_at = $1 + WHERE id = $2 AND user_id = $3 + `, oldActivity, sessionID, userID) + require.NoError(t, err) + + err = client.StoreTask(ctx, &protocol.Task{ + ID: "task-1", + ContextID: sessionID, + }) + require.NoError(t, err) + + got, err := client.GetSession(ctx, sessionID, userID) + require.NoError(t, err) + assert.True(t, got.UpdatedAt.After(oldActivity), "session updated_at should advance after storing a task") +} + // TestStoreAgentIdempotence verifies that calling StoreAgent multiple times // with the same data is idempotent and doesn't error. This is critical for // the lock-free concurrency model where concurrent upserts must succeed. diff --git a/go/core/internal/database/gen/querier.go b/go/core/internal/database/gen/querier.go index e58850e00..d8e6c112b 100644 --- a/go/core/internal/database/gen/querier.go +++ b/go/core/internal/database/gen/querier.go @@ -61,6 +61,8 @@ type Querier interface { SoftDeleteToolServer(ctx context.Context, arg SoftDeleteToolServerParams) error SoftDeleteToolsForServer(ctx context.Context, arg SoftDeleteToolsForServerParams) error TaskExists(ctx context.Context, id string) (bool, error) + TouchSession(ctx context.Context, id string) error + TouchSessionForUser(ctx context.Context, arg TouchSessionForUserParams) error UpsertAgent(ctx context.Context, arg UpsertAgentParams) error UpsertCheckpoint(ctx context.Context, arg UpsertCheckpointParams) error UpsertCheckpointWrite(ctx context.Context, arg UpsertCheckpointWriteParams) error diff --git a/go/core/internal/database/gen/sessions.sql.go b/go/core/internal/database/gen/sessions.sql.go index 4b44ddeb1..006d2c762 100644 --- a/go/core/internal/database/gen/sessions.sql.go +++ b/go/core/internal/database/gen/sessions.sql.go @@ -39,7 +39,7 @@ func (q *Queries) GetSession(ctx context.Context, arg GetSessionParams) (Session const listSessions = `-- name: ListSessions :many SELECT id, user_id, name, created_at, updated_at, deleted_at, agent_id, source FROM session WHERE user_id = $1 AND deleted_at IS NULL -ORDER BY created_at ASC +ORDER BY updated_at DESC, created_at DESC ` func (q *Queries) ListSessions(ctx context.Context, userID string) ([]Session, error) { @@ -75,7 +75,7 @@ const listSessionsForAgent = `-- name: ListSessionsForAgent :many SELECT id, user_id, name, created_at, updated_at, deleted_at, agent_id, source FROM session WHERE agent_id = $1 AND user_id = $2 AND deleted_at IS NULL AND (source IS NULL OR source != 'agent') -ORDER BY created_at ASC +ORDER BY updated_at DESC, created_at DESC ` type ListSessionsForAgentParams struct { @@ -116,7 +116,7 @@ const listSessionsForAgentAllUsers = `-- name: ListSessionsForAgentAllUsers :man SELECT id, user_id, name, created_at, updated_at, deleted_at, agent_id, source FROM session WHERE agent_id = $1 AND deleted_at IS NULL AND (source IS NULL OR source != 'agent') -ORDER BY created_at ASC +ORDER BY updated_at DESC, created_at DESC ` func (q *Queries) ListSessionsForAgentAllUsers(ctx context.Context, agentID *string) ([]Session, error) { @@ -163,6 +163,31 @@ func (q *Queries) SoftDeleteSession(ctx context.Context, arg SoftDeleteSessionPa return err } +const touchSession = `-- name: TouchSession :exec +UPDATE session SET updated_at = NOW() +WHERE id = $1 AND deleted_at IS NULL +` + +func (q *Queries) TouchSession(ctx context.Context, id string) error { + _, err := q.db.Exec(ctx, touchSession, id) + return err +} + +const touchSessionForUser = `-- name: TouchSessionForUser :exec +UPDATE session SET updated_at = NOW() +WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL +` + +type TouchSessionForUserParams struct { + ID string + UserID string +} + +func (q *Queries) TouchSessionForUser(ctx context.Context, arg TouchSessionForUserParams) error { + _, err := q.db.Exec(ctx, touchSessionForUser, arg.ID, arg.UserID) + return err +} + const upsertSession = `-- name: UpsertSession :exec INSERT INTO session (id, user_id, name, agent_id, source, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) diff --git a/go/core/internal/database/queries/sessions.sql b/go/core/internal/database/queries/sessions.sql index a9d8c9eb2..a9976fb85 100644 --- a/go/core/internal/database/queries/sessions.sql +++ b/go/core/internal/database/queries/sessions.sql @@ -6,19 +6,19 @@ LIMIT 1; -- name: ListSessions :many SELECT * FROM session WHERE user_id = $1 AND deleted_at IS NULL -ORDER BY created_at ASC; +ORDER BY updated_at DESC, created_at DESC; -- name: ListSessionsForAgent :many SELECT * FROM session WHERE agent_id = $1 AND user_id = $2 AND deleted_at IS NULL AND (source IS NULL OR source != 'agent') -ORDER BY created_at ASC; +ORDER BY updated_at DESC, created_at DESC; -- name: ListSessionsForAgentAllUsers :many SELECT * FROM session WHERE agent_id = $1 AND deleted_at IS NULL AND (source IS NULL OR source != 'agent') -ORDER BY created_at ASC; +ORDER BY updated_at DESC, created_at DESC; -- name: UpsertSession :exec INSERT INTO session (id, user_id, name, agent_id, source, created_at, updated_at) @@ -32,3 +32,11 @@ ON CONFLICT (id, user_id) DO UPDATE SET -- name: SoftDeleteSession :exec UPDATE session SET deleted_at = NOW() WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL; + +-- name: TouchSessionForUser :exec +UPDATE session SET updated_at = NOW() +WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL; + +-- name: TouchSession :exec +UPDATE session SET updated_at = NOW() +WHERE id = $1 AND deleted_at IS NULL; diff --git a/ui/src/components/sidebars/GroupedChats.stories.tsx b/ui/src/components/sidebars/GroupedChats.stories.tsx index 2c5e4927c..5df53ebbb 100644 --- a/ui/src/components/sidebars/GroupedChats.stories.tsx +++ b/ui/src/components/sidebars/GroupedChats.stories.tsx @@ -27,13 +27,13 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const createSession = (id: string, name: string, daysAgo: number): Session => ({ +const createSession = (id: string, name: string, createdDaysAgo: number, updatedDaysAgo = createdDaysAgo): Session => ({ id, name, agent_id: 'kgent__NS__k8s', user_id: "user-1", - created_at: new Date(Date.now() - daysAgo * 24 * 3600000).toISOString(), - updated_at: new Date(Date.now() - daysAgo * 24 * 3600000).toISOString(), + created_at: new Date(Date.now() - createdDaysAgo * 24 * 3600000).toISOString(), + updated_at: new Date(Date.now() - updatedDaysAgo * 24 * 3600000).toISOString(), deleted_at: "", }); @@ -81,3 +81,15 @@ export const ManySessions: Story = { ], }, }; + +export const RecentlyUpdatedOlderSession: Story = { + args: { + agentName: "k8s", + agentNamespace: "kagent", + sessions: [ + createSession("session-old-active", "Created last week, active today", 7, 0), + createSession("session-new-inactive", "Created today, inactive", 0, 0.2), + createSession("session-yesterday", "Yesterday chat", 1), + ], + }, +}; diff --git a/ui/src/components/sidebars/GroupedChats.tsx b/ui/src/components/sidebars/GroupedChats.tsx index 6e63a72f0..5ac0048a9 100644 --- a/ui/src/components/sidebars/GroupedChats.tsx +++ b/ui/src/components/sidebars/GroupedChats.tsx @@ -39,9 +39,11 @@ export default function GroupedChats({ agentName, agentNamespace, sessions, hide older: [], }; - // Process each session and group by date + const getActivityDate = (session: Session) => new Date(session.updated_at || session.created_at); + + // Process each session and group by last activity date localSessions.forEach(session => { - const date = new Date(session.created_at); + const date = getActivityDate(session); if (isToday(date)) { groups.today.push(session); } else if (isYesterday(date)) { @@ -53,11 +55,7 @@ export default function GroupedChats({ agentName, agentNamespace, sessions, hide const sortChats = (sessions: Session[]) => sessions.sort((a, b) => { - const getLatestTimestamp = (session: Session) => { - return new Date(session.created_at).getTime(); - }; - - return getLatestTimestamp(b) - getLatestTimestamp(a); + return getActivityDate(b).getTime() - getActivityDate(a).getTime(); }); return { diff --git a/ui/src/components/sidebars/SessionGroup.tsx b/ui/src/components/sidebars/SessionGroup.tsx index e997f3260..fbf33f340 100644 --- a/ui/src/components/sidebars/SessionGroup.tsx +++ b/ui/src/components/sidebars/SessionGroup.tsx @@ -30,7 +30,7 @@ const ChatGroup = ({ title, sessions, onDeleteSession, onDownloadSession, agentN {sessions.map((session) => ( - + ))} From 792132e27dbdc0b5b16a4a216128357db6143eb2 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Sun, 26 Apr 2026 03:34:53 +0530 Subject: [PATCH 2/4] fix: address session activity review feedback Signed-off-by: Asish Kumar --- go/core/internal/database/client_postgres.go | 50 ++++++------------- go/core/internal/database/gen/events.sql.go | 9 ++++ go/core/internal/database/gen/querier.go | 2 - go/core/internal/database/gen/sessions.sql.go | 25 ---------- go/core/internal/database/gen/tasks.sql.go | 8 +++ go/core/internal/database/queries/events.sql | 11 +++- .../internal/database/queries/sessions.sql | 8 --- go/core/internal/database/queries/tasks.sql | 10 +++- .../components/sidebars/ChatItem.stories.tsx | 8 +-- ui/src/components/sidebars/ChatItem.tsx | 6 +-- ui/src/components/sidebars/SessionGroup.tsx | 2 +- 11 files changed, 58 insertions(+), 81 deletions(-) diff --git a/go/core/internal/database/client_postgres.go b/go/core/internal/database/client_postgres.go index e0fc77696..15a2dbacd 100644 --- a/go/core/internal/database/client_postgres.go +++ b/go/core/internal/database/client_postgres.go @@ -148,28 +148,17 @@ func (c *postgresClient) DeleteSession(ctx context.Context, sessionID, userID st // ── Events ──────────────────────────────────────────────────────────────────── func (c *postgresClient) StoreEvents(ctx context.Context, events ...*dbpkg.Event) error { - return c.withTx(ctx, func(q *dbgen.Queries) error { - for _, e := range events { - sessionID := strPtrIfNotEmpty(e.SessionID) - if err := q.InsertEvent(ctx, dbgen.InsertEventParams{ - ID: e.ID, - UserID: e.UserID, - SessionID: sessionID, - Data: e.Data, - }); err != nil { - return fmt.Errorf("failed to store event %s: %w", e.ID, err) - } - if sessionID != nil { - if err := q.TouchSessionForUser(ctx, dbgen.TouchSessionForUserParams{ - ID: *sessionID, - UserID: e.UserID, - }); err != nil { - return fmt.Errorf("failed to touch session %s: %w", *sessionID, err) - } - } + for _, e := range events { + if err := c.q.InsertEvent(ctx, dbgen.InsertEventParams{ + ID: e.ID, + UserID: e.UserID, + SessionID: strPtrIfNotEmpty(e.SessionID), + Data: e.Data, + }); err != nil { + return fmt.Errorf("failed to store event %s: %w", e.ID, err) } - return nil - }) + } + return nil } func (c *postgresClient) ListEventsForSession(ctx context.Context, sessionID, userID string, opts dbpkg.QueryOptions) ([]*dbpkg.Event, error) { @@ -213,21 +202,10 @@ func (c *postgresClient) StoreTask(ctx context.Context, task *protocol.Task) err if err != nil { return fmt.Errorf("failed to serialize task: %w", err) } - return c.withTx(ctx, func(q *dbgen.Queries) error { - sessionID := strPtrIfNotEmpty(task.ContextID) - if err := q.UpsertTask(ctx, dbgen.UpsertTaskParams{ - ID: task.ID, - Data: string(data), - SessionID: sessionID, - }); err != nil { - return err - } - if sessionID != nil { - if err := q.TouchSession(ctx, *sessionID); err != nil { - return fmt.Errorf("failed to touch session %s: %w", *sessionID, err) - } - } - return nil + return c.q.UpsertTask(ctx, dbgen.UpsertTaskParams{ + ID: task.ID, + Data: string(data), + SessionID: strPtrIfNotEmpty(task.ContextID), }) } diff --git a/go/core/internal/database/gen/events.sql.go b/go/core/internal/database/gen/events.sql.go index 4c16cfd3e..af5a00727 100644 --- a/go/core/internal/database/gen/events.sql.go +++ b/go/core/internal/database/gen/events.sql.go @@ -37,8 +37,17 @@ func (q *Queries) GetEvent(ctx context.Context, arg GetEventParams) (Event, erro } const insertEvent = `-- name: InsertEvent :exec +WITH inserted_event AS ( INSERT INTO event (id, user_id, session_id, data, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW()) +RETURNING user_id, session_id +) +UPDATE session +SET updated_at = NOW() +FROM inserted_event +WHERE session.id = inserted_event.session_id + AND session.user_id = inserted_event.user_id + AND session.deleted_at IS NULL ` type InsertEventParams struct { diff --git a/go/core/internal/database/gen/querier.go b/go/core/internal/database/gen/querier.go index d8e6c112b..e58850e00 100644 --- a/go/core/internal/database/gen/querier.go +++ b/go/core/internal/database/gen/querier.go @@ -61,8 +61,6 @@ type Querier interface { SoftDeleteToolServer(ctx context.Context, arg SoftDeleteToolServerParams) error SoftDeleteToolsForServer(ctx context.Context, arg SoftDeleteToolsForServerParams) error TaskExists(ctx context.Context, id string) (bool, error) - TouchSession(ctx context.Context, id string) error - TouchSessionForUser(ctx context.Context, arg TouchSessionForUserParams) error UpsertAgent(ctx context.Context, arg UpsertAgentParams) error UpsertCheckpoint(ctx context.Context, arg UpsertCheckpointParams) error UpsertCheckpointWrite(ctx context.Context, arg UpsertCheckpointWriteParams) error diff --git a/go/core/internal/database/gen/sessions.sql.go b/go/core/internal/database/gen/sessions.sql.go index 006d2c762..24490bc62 100644 --- a/go/core/internal/database/gen/sessions.sql.go +++ b/go/core/internal/database/gen/sessions.sql.go @@ -163,31 +163,6 @@ func (q *Queries) SoftDeleteSession(ctx context.Context, arg SoftDeleteSessionPa return err } -const touchSession = `-- name: TouchSession :exec -UPDATE session SET updated_at = NOW() -WHERE id = $1 AND deleted_at IS NULL -` - -func (q *Queries) TouchSession(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, touchSession, id) - return err -} - -const touchSessionForUser = `-- name: TouchSessionForUser :exec -UPDATE session SET updated_at = NOW() -WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL -` - -type TouchSessionForUserParams struct { - ID string - UserID string -} - -func (q *Queries) TouchSessionForUser(ctx context.Context, arg TouchSessionForUserParams) error { - _, err := q.db.Exec(ctx, touchSessionForUser, arg.ID, arg.UserID) - return err -} - const upsertSession = `-- name: UpsertSession :exec INSERT INTO session (id, user_id, name, agent_id, source, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) diff --git a/go/core/internal/database/gen/tasks.sql.go b/go/core/internal/database/gen/tasks.sql.go index f5e8f8d2d..1c7c34851 100644 --- a/go/core/internal/database/gen/tasks.sql.go +++ b/go/core/internal/database/gen/tasks.sql.go @@ -85,12 +85,20 @@ func (q *Queries) TaskExists(ctx context.Context, id string) (bool, error) { } const upsertTask = `-- name: UpsertTask :exec +WITH upserted_task AS ( INSERT INTO task (id, data, session_id, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data, session_id = EXCLUDED.session_id, updated_at = NOW() +RETURNING session_id +) +UPDATE session +SET updated_at = NOW() +FROM upserted_task +WHERE session.id = upserted_task.session_id + AND session.deleted_at IS NULL ` type UpsertTaskParams struct { diff --git a/go/core/internal/database/queries/events.sql b/go/core/internal/database/queries/events.sql index ae6fd88dc..51c0c20fe 100644 --- a/go/core/internal/database/queries/events.sql +++ b/go/core/internal/database/queries/events.sql @@ -1,6 +1,15 @@ -- name: InsertEvent :exec +WITH inserted_event AS ( INSERT INTO event (id, user_id, session_id, data, created_at, updated_at) -VALUES ($1, $2, $3, $4, NOW(), NOW()); +VALUES ($1, $2, $3, $4, NOW(), NOW()) +RETURNING user_id, session_id +) +UPDATE session +SET updated_at = NOW() +FROM inserted_event +WHERE session.id = inserted_event.session_id + AND session.user_id = inserted_event.user_id + AND session.deleted_at IS NULL; -- name: GetEvent :one SELECT * FROM event diff --git a/go/core/internal/database/queries/sessions.sql b/go/core/internal/database/queries/sessions.sql index a9976fb85..98fdda572 100644 --- a/go/core/internal/database/queries/sessions.sql +++ b/go/core/internal/database/queries/sessions.sql @@ -32,11 +32,3 @@ ON CONFLICT (id, user_id) DO UPDATE SET -- name: SoftDeleteSession :exec UPDATE session SET deleted_at = NOW() WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL; - --- name: TouchSessionForUser :exec -UPDATE session SET updated_at = NOW() -WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL; - --- name: TouchSession :exec -UPDATE session SET updated_at = NOW() -WHERE id = $1 AND deleted_at IS NULL; diff --git a/go/core/internal/database/queries/tasks.sql b/go/core/internal/database/queries/tasks.sql index ae72627c1..b1497eeee 100644 --- a/go/core/internal/database/queries/tasks.sql +++ b/go/core/internal/database/queries/tasks.sql @@ -14,12 +14,20 @@ WHERE session_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC; -- name: UpsertTask :exec +WITH upserted_task AS ( INSERT INTO task (id, data, session_id, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data, session_id = EXCLUDED.session_id, - updated_at = NOW(); + updated_at = NOW() +RETURNING session_id +) +UPDATE session +SET updated_at = NOW() +FROM upserted_task +WHERE session.id = upserted_task.session_id + AND session.deleted_at IS NULL; -- name: SoftDeleteTask :exec UPDATE task SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL; diff --git a/ui/src/components/sidebars/ChatItem.stories.tsx b/ui/src/components/sidebars/ChatItem.stories.tsx index 406405a2f..0b40936f4 100644 --- a/ui/src/components/sidebars/ChatItem.stories.tsx +++ b/ui/src/components/sidebars/ChatItem.stories.tsx @@ -62,7 +62,7 @@ export const ShortTitle: Story = { agentNamespace: "kagent", onDelete: async () => {}, sessionName: "Quick question", - createdAt: new Date().toISOString(), + activityAt: new Date().toISOString(), }, }; @@ -73,7 +73,7 @@ export const LongTitle: Story = { agentNamespace: "kagent", onDelete: async () => {}, sessionName: "Review https://github.com/Smartest-Fly/app/pull/1234 and provide feedback on the authentication implementation", - createdAt: new Date().toISOString(), + activityAt: new Date().toISOString(), }, }; @@ -84,7 +84,7 @@ export const LongTitleWithAgentName: Story = { agentNamespace: "kagent", onDelete: async () => {}, sessionName: "Review https://github.com/Smartest-Fly/app/pull/1234 and provide feedback on the authentication implementation", - createdAt: new Date().toISOString(), + activityAt: new Date().toISOString(), }, }; @@ -107,7 +107,7 @@ export const MultipleLongTitles: Story = { agentNamespace="kagent" onDelete={async () => {}} sessionName={title} - createdAt={new Date(Date.now() - i * 3600000).toISOString()} + activityAt={new Date(Date.now() - i * 3600000).toISOString()} /> ))} diff --git a/ui/src/components/sidebars/ChatItem.tsx b/ui/src/components/sidebars/ChatItem.tsx index 76b7ebdfe..cf2b3220e 100644 --- a/ui/src/components/sidebars/ChatItem.tsx +++ b/ui/src/components/sidebars/ChatItem.tsx @@ -22,12 +22,12 @@ interface ChatItemProps { agentNamespace?: string; sessionName?: string; onDownload?: (sessionId: string) => Promise; - createdAt?: string; + activityAt?: string; /** When true, omit delete (e.g. Sandbox single-session agents). */ hideDelete?: boolean; } -const ChatItem = ({ sessionId, agentName, agentNamespace, onDelete, sessionName, onDownload, createdAt, hideDelete }: ChatItemProps) => { +const ChatItem = ({ sessionId, agentName, agentNamespace, onDelete, sessionName, onDownload, activityAt, hideDelete }: ChatItemProps) => { const title = sessionName || "Untitled"; // Format timestamp based on how recent it is @@ -60,7 +60,7 @@ const ChatItem = ({ sessionId, agentName, agentNamespace, onDelete, sessionName, style={{ background: 'linear-gradient(to right, transparent, hsl(var(--sidebar-background)) 30%)', }} - >{formatTime(createdAt)} + >{formatTime(activityAt)} diff --git a/ui/src/components/sidebars/SessionGroup.tsx b/ui/src/components/sidebars/SessionGroup.tsx index fbf33f340..43f1c9826 100644 --- a/ui/src/components/sidebars/SessionGroup.tsx +++ b/ui/src/components/sidebars/SessionGroup.tsx @@ -30,7 +30,7 @@ const ChatGroup = ({ title, sessions, onDeleteSession, onDownloadSession, agentN {sessions.map((session) => ( - + ))} From f5d2cc0fa74b6d2632bc58dbda5b7cd0be9ed703 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Mon, 4 May 2026 11:52:45 +0530 Subject: [PATCH 3/4] fix: address remaining activity review feedback Signed-off-by: Asish Kumar --- go/core/internal/database/gen/events.sql.go | 3 +- go/core/internal/database/gen/tasks.sql.go | 3 +- go/core/internal/database/queries/events.sql | 3 +- go/core/internal/database/queries/tasks.sql | 3 +- ui/src/components/sidebars/GroupedChats.tsx | 34 ++++++++++++-------- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/go/core/internal/database/gen/events.sql.go b/go/core/internal/database/gen/events.sql.go index af5a00727..d34d42298 100644 --- a/go/core/internal/database/gen/events.sql.go +++ b/go/core/internal/database/gen/events.sql.go @@ -45,7 +45,8 @@ RETURNING user_id, session_id UPDATE session SET updated_at = NOW() FROM inserted_event -WHERE session.id = inserted_event.session_id +WHERE inserted_event.session_id IS NOT NULL + AND session.id = inserted_event.session_id AND session.user_id = inserted_event.user_id AND session.deleted_at IS NULL ` diff --git a/go/core/internal/database/gen/tasks.sql.go b/go/core/internal/database/gen/tasks.sql.go index 1c7c34851..e91be6daa 100644 --- a/go/core/internal/database/gen/tasks.sql.go +++ b/go/core/internal/database/gen/tasks.sql.go @@ -97,7 +97,8 @@ RETURNING session_id UPDATE session SET updated_at = NOW() FROM upserted_task -WHERE session.id = upserted_task.session_id +WHERE upserted_task.session_id IS NOT NULL + AND session.id = upserted_task.session_id AND session.deleted_at IS NULL ` diff --git a/go/core/internal/database/queries/events.sql b/go/core/internal/database/queries/events.sql index 51c0c20fe..9f916a03d 100644 --- a/go/core/internal/database/queries/events.sql +++ b/go/core/internal/database/queries/events.sql @@ -7,7 +7,8 @@ RETURNING user_id, session_id UPDATE session SET updated_at = NOW() FROM inserted_event -WHERE session.id = inserted_event.session_id +WHERE inserted_event.session_id IS NOT NULL + AND session.id = inserted_event.session_id AND session.user_id = inserted_event.user_id AND session.deleted_at IS NULL; diff --git a/go/core/internal/database/queries/tasks.sql b/go/core/internal/database/queries/tasks.sql index b1497eeee..66105c6f3 100644 --- a/go/core/internal/database/queries/tasks.sql +++ b/go/core/internal/database/queries/tasks.sql @@ -26,7 +26,8 @@ RETURNING session_id UPDATE session SET updated_at = NOW() FROM upserted_task -WHERE session.id = upserted_task.session_id +WHERE upserted_task.session_id IS NOT NULL + AND session.id = upserted_task.session_id AND session.deleted_at IS NULL; -- name: SoftDeleteTask :exec diff --git a/ui/src/components/sidebars/GroupedChats.tsx b/ui/src/components/sidebars/GroupedChats.tsx index 5ac0048a9..095acd54d 100644 --- a/ui/src/components/sidebars/GroupedChats.tsx +++ b/ui/src/components/sidebars/GroupedChats.tsx @@ -29,34 +29,42 @@ export default function GroupedChats({ agentName, agentNamespace, sessions, hide }, [sessions]); const groupedChats = useMemo(() => { + type SessionWithActivity = { + session: Session; + activityTimestamp: number; + }; + const groups: { - today: Session[]; - yesterday: Session[]; - older: Session[]; + today: SessionWithActivity[]; + yesterday: SessionWithActivity[]; + older: SessionWithActivity[]; } = { today: [], yesterday: [], older: [], }; - const getActivityDate = (session: Session) => new Date(session.updated_at || session.created_at); + const sessionsWithActivity = localSessions.map(session => ({ + session, + activityTimestamp: Date.parse(session.updated_at || session.created_at), + })); // Process each session and group by last activity date - localSessions.forEach(session => { - const date = getActivityDate(session); + sessionsWithActivity.forEach(sessionWithActivity => { + const date = new Date(sessionWithActivity.activityTimestamp); if (isToday(date)) { - groups.today.push(session); + groups.today.push(sessionWithActivity); } else if (isYesterday(date)) { - groups.yesterday.push(session); + groups.yesterday.push(sessionWithActivity); } else { - groups.older.push(session); + groups.older.push(sessionWithActivity); } }); - const sortChats = (sessions: Session[]) => - sessions.sort((a, b) => { - return getActivityDate(b).getTime() - getActivityDate(a).getTime(); - }); + const sortChats = (sessions: SessionWithActivity[]) => + sessions + .sort((a, b) => b.activityTimestamp - a.activityTimestamp) + .map(({ session }) => session); return { today: sortChats(groups.today), From 18175038c8a6cd27fc7a2a3d40ef052ccbaf2d0e Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 5 May 2026 13:50:21 +0530 Subject: [PATCH 4/4] test: align session handler ordering expectations Signed-off-by: Asish Kumar --- .../httpserver/handlers/sessions_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/go/core/internal/httpserver/handlers/sessions_test.go b/go/core/internal/httpserver/handlers/sessions_test.go index 517ea3682..02ee3243d 100644 --- a/go/core/internal/httpserver/handlers/sessions_test.go +++ b/go/core/internal/httpserver/handlers/sessions_test.go @@ -79,15 +79,28 @@ func TestSessionsHandler(t *testing.T) { return session } + setSessionActivity := func(t *testing.T, sessionID, userID string, createdAt, updatedAt time.Time) { + t.Helper() + _, err := sharedDB.Exec(context.Background(), ` + UPDATE session + SET created_at = $1, updated_at = $2 + WHERE id = $3 AND user_id = $4 + `, createdAt, updatedAt, sessionID, userID) + require.NoError(t, err) + } + t.Run("HandleListSessions", func(t *testing.T) { t.Run("Success", func(t *testing.T) { handler, dbClient, responseRecorder := setupHandler(t) userID := "test-user" + base := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) // Create test sessions agentID := "1" session1 := createTestSession(t, dbClient, "session-1", userID, agentID) session2 := createTestSession(t, dbClient, "session-2", userID, agentID) + setSessionActivity(t, session1.ID, userID, base, base.Add(2*time.Hour)) + setSessionActivity(t, session2.ID, userID, base.Add(time.Hour), base.Add(time.Hour)) req := httptest.NewRequest("GET", "/api/sessions?user_id="+userID, nil) req = setUser(req, userID) @@ -472,6 +485,9 @@ func TestSessionsHandler(t *testing.T) { agent := createTestAgent(t, dbClient, agentRef) session1 := createTestSession(t, dbClient, "session-1", userID, agent.ID) session2 := createTestSession(t, dbClient, "session-2", userID, agent.ID) + base := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + setSessionActivity(t, session1.ID, userID, base, base.Add(2*time.Hour)) + setSessionActivity(t, session2.ID, userID, base.Add(time.Hour), base.Add(time.Hour)) req := httptest.NewRequest("GET", "/api/agents/"+namespace+"/"+agentName+"/sessions", nil) req = mux.SetURLVars(req, map[string]string{"namespace": namespace, "name": agentName})