Skip to content

Commit 27941cb

Browse files
committed
fix(activity): null-check each title candidate independently
The report title fallback wrapped display_name and session_name in a single COALESCE before NULLIF. A session renamed to an empty string (literal '' display_name) with a valid session_name resolved to '', which NULLIF turned into NULL, skipping the real session_name and falling through to first_message. NULLIF each candidate independently so an empty display_name cannot mask a set session_name. Applied identically to SQLite, PostgreSQL, and DuckDB to keep the title expression in backend parity.
1 parent 3955310 commit 27941cb

4 files changed

Lines changed: 37 additions & 3 deletions

File tree

internal/db/activityreport.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,12 @@ func (db *DB) activityReportSessions(
8484
where, args := f.buildWhereWithDate("", false)
8585
args = append(args, rangeStartUTC, rangeEndUTC)
8686

87+
// Each Title candidate is NULLIF'd independently (not a nested
88+
// COALESCE-then-NULLIF) so an empty display_name cannot mask a real
89+
// session_name.
8790
query := `SELECT
8891
s.id,
89-
COALESCE(NULLIF(COALESCE(s.display_name, s.session_name), ''),
92+
COALESCE(NULLIF(s.display_name, ''), NULLIF(s.session_name, ''),
9093
NULLIF(s.first_message, ''), NULLIF(s.project, ''), s.id),
9194
s.project,
9295
s.agent,

internal/db/activityreport_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,31 @@ func TestGetActivityReport_UsageDedupSubSecondOrder(t *testing.T) {
382382
assert.Equal(t, 500, r.Totals.OutputTokens,
383383
"first-seen dedup keeps the chronologically earlier whole-second row")
384384
}
385+
386+
// TestGetActivityReport_TitleSkipsEmptyDisplayName confirms the Title fallback
387+
// null-checks each candidate independently: an empty (non-NULL) display_name
388+
// must not mask a real session_name. A nested COALESCE(display_name,
389+
// session_name) would return '' and be NULLIF'd away, wrongly skipping to
390+
// first_message. RenameSession stores a literal '' (only nil clears to NULL),
391+
// so this reproduces a session renamed to "" that still has a session_name.
392+
func TestGetActivityReport_TitleSkipsEmptyDisplayName(t *testing.T) {
393+
d := testDB(t)
394+
ctx := context.Background()
395+
insertSession(t, d, "s", "proj", func(s *Session) {
396+
s.Agent = "claude"
397+
s.SessionName = Ptr("real-session-name")
398+
s.FirstMessage = Ptr("first message text")
399+
s.StartedAt = Ptr("2026-06-16T10:00:00Z")
400+
s.EndedAt = Ptr("2026-06-16T10:02:00Z")
401+
})
402+
require.NoError(t, d.RenameSession("s", Ptr("")))
403+
seedMessage(t, d, "s", 1, "user", "2026-06-16T10:00:00Z", "")
404+
seedMessage(t, d, "s", 2, "assistant", "2026-06-16T10:02:00Z", "opus")
405+
406+
r, err := d.GetActivityReport(ctx, AnalyticsFilter{Timezone: "UTC"},
407+
dayQuery(t, "2026-06-16", "UTC"))
408+
require.NoError(t, err)
409+
require.Len(t, r.BySession, 1)
410+
assert.Equal(t, "real-session-name", r.BySession[0].Title,
411+
"empty display_name must not mask the real session_name")
412+
}

internal/duckdb/activityreport.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func (s *Store) activityReportSessions(
8686

8787
query := `SELECT
8888
s.id,
89-
COALESCE(NULLIF(COALESCE(s.display_name, s.session_name), ''), NULLIF(s.first_message, ''), NULLIF(s.project, ''), s.id) AS display_name,
89+
COALESCE(NULLIF(s.display_name, ''), NULLIF(s.session_name, ''), NULLIF(s.first_message, ''), NULLIF(s.project, ''), s.id) AS display_name,
9090
s.project,
9191
s.agent,
9292
s.machine,

internal/postgres/activityreport.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,12 @@ func (s *Store) activityReportSessions(
7979
lower := pb.add(rangeStartUTC)
8080
upper := pb.add(rangeEndUTC)
8181

82+
// Each Title candidate is NULLIF'd independently (not a nested
83+
// COALESCE-then-NULLIF) so an empty display_name cannot mask a real
84+
// session_name.
8285
query := `SELECT
8386
s.id,
84-
COALESCE(NULLIF(COALESCE(s.display_name, s.session_name), ''), NULLIF(s.first_message, ''), NULLIF(s.project, ''), s.id) AS display_name,
87+
COALESCE(NULLIF(s.display_name, ''), NULLIF(s.session_name, ''), NULLIF(s.first_message, ''), NULLIF(s.project, ''), s.id) AS display_name,
8588
s.project,
8689
s.agent,
8790
s.machine,

0 commit comments

Comments
 (0)