From a88affb669fcc075a211cf218849507d8eb243c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 09:27:38 +0000 Subject: [PATCH] Exclude threads with stale pre-2000 lastSignalAt from thread list API Filters both the vector-search and normal listThreads response paths. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011Y24U8yakRkcpucyjkpChh --- src/api/threadsApi.ts | 13 ++++++++++--- tests/api/api.spec.ts | 12 ++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/api/threadsApi.ts b/src/api/threadsApi.ts index 97a5a99b..0def337d 100644 --- a/src/api/threadsApi.ts +++ b/src/api/threadsApi.ts @@ -11,7 +11,7 @@ import { buildScheduleName } from "../scheduler/schedule-name.js"; import { durationToSeconds } from "../retention.js"; import { isCalendarEventSignal, isEmailSignal } from "../types/index.js"; import type { EmailContentStore } from "./content-store.js"; -import type { Signal, AnySignal, InboundEmailSignalData, PageParams, ThreadStatus, Workflow } from "../types/index.js"; +import type { Signal, AnySignal, InboundEmailSignalData, PageParams, Thread, ThreadStatus, Workflow } from "../types/index.js"; import type { CalendarResponseData, DomainMisconfigurationData, Pagination } from "../types/index.js"; import type { UpdateThreadFields, ThreadDatabase } from "../database/thread-database.js"; import type { AccountDatabase } from "../database/account-database.js"; @@ -51,6 +51,13 @@ function page(key: K, items: T[], nextCursor?: string): Rec return { [key]: items, pagination: { cursor: nextCursor ?? null } } as Record & { pagination: Pagination }; } +// Threads with a stale/placeholder lastSignalAt (e.g. never-updated legacy records) are +// excluded from list responses — they don't represent real activity. +const MIN_LAST_SIGNAL_AT = "2000-01-01T00:00:00.000Z"; +function hasRecentSignal(thread: Thread): boolean { + return thread.lastSignalAt >= MIN_LAST_SIGNAL_AT; +} + export class ThreadsApi { constructor( private readonly threadDb: ThreadDatabase, @@ -129,7 +136,7 @@ export class ThreadsApi { return err(c, 500, "Internal Server Error"); } - return c.json(page("threads", threadsResult.value.map(toApiThread), undefined), 200); + return c.json(page("threads", threadsResult.value.filter(hasRecentSignal).map(toApiThread), undefined), 200); } const params: ListThreadsParams = { ...(query["workflow"] ? { workflow: query["workflow"] as Workflow } : {}), @@ -143,7 +150,7 @@ export class ThreadsApi { logger.error(`Failed to list threads: ${result.error.message}`, { code: "api.threads.list_failed", error: result.error }); return err(c, 500, "Internal Server Error"); } - return c.json(page("threads", result.value.items.map(toApiThread), result.value.nextCursor), 200); + return c.json(page("threads", result.value.items.filter(hasRecentSignal).map(toApiThread), result.value.nextCursor), 200); }); // ------------------------------------------------------------------------- diff --git a/tests/api/api.spec.ts b/tests/api/api.spec.ts index 626b1403..6d758a38 100644 --- a/tests/api/api.spec.ts +++ b/tests/api/api.spec.ts @@ -415,6 +415,18 @@ describe("API", () => { const body = await res.json() as { pagination: { cursor: string } }; expect(body.pagination.cursor).toBe("cursor-abc"); }); + + it("excludes threads whose last signal predates Jan 1 2000", async () => { + vi.mocked(threadDb.listThreads).mockResolvedValueOnce(ok({ + items: [ + makeThread({ id: "arc-stale", lastSignalAt: "1999-12-31T23:59:59.000Z" }), + makeThread({ id: "arc-fresh", lastSignalAt: "2024-01-01T00:00:00.000Z" }), + ], + })); + const res = await req(app, "GET", `${A}/threads`); + const body = await res.json() as { threads: { threadId: string }[] }; + expect(body.threads.map(t => t.threadId)).toEqual(["arc-fresh"]); + }); }); describe("GET /accounts/:accountId/threads/:threadId", () => {