-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(webapp): show sessions with no live run as Idle instead of Active #4570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a1ffd4a
799dbb4
e67dc2b
5b9c8dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: fix | ||
| --- | ||
|
|
||
| The Sessions list no longer shows an abandoned session as Active with a duration that climbs forever. A session whose run has finished now shows as Idle with a duration frozen at when it stopped, and only sessions with a run still executing show as Active. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { deriveSessionStatus } from "./deriveSessionStatus"; | ||
|
|
||
| const NOW = new Date("2026-08-06T12:00:00.000Z").getTime(); | ||
| const PAST = new Date("2026-08-01T00:00:00.000Z"); | ||
| const FUTURE = new Date("2026-08-10T00:00:00.000Z"); | ||
|
|
||
| describe("deriveSessionStatus", () => { | ||
| it("returns CLOSED when closedAt is set, even with a live run", () => { | ||
| expect( | ||
| deriveSessionStatus({ | ||
| closedAt: PAST, | ||
| expiresAt: null, | ||
| hasCurrentRun: true, | ||
| currentRunStatus: "EXECUTING", | ||
| now: NOW, | ||
| }) | ||
| ).toBe("CLOSED"); | ||
| }); | ||
|
|
||
| it("prefers CLOSED over an elapsed expiresAt", () => { | ||
| expect( | ||
| deriveSessionStatus({ | ||
| closedAt: PAST, | ||
| expiresAt: PAST, | ||
| hasCurrentRun: false, | ||
| currentRunStatus: undefined, | ||
| now: NOW, | ||
| }) | ||
| ).toBe("CLOSED"); | ||
| }); | ||
|
|
||
| it("returns EXPIRED when expiresAt is in the past", () => { | ||
| expect( | ||
| deriveSessionStatus({ | ||
| closedAt: null, | ||
| expiresAt: PAST, | ||
| hasCurrentRun: true, | ||
| currentRunStatus: "EXECUTING", | ||
| now: NOW, | ||
| }) | ||
| ).toBe("EXPIRED"); | ||
| }); | ||
|
|
||
| it("returns ACTIVE when the current run is non-final", () => { | ||
| expect( | ||
| deriveSessionStatus({ | ||
| closedAt: null, | ||
| expiresAt: FUTURE, | ||
| hasCurrentRun: true, | ||
| currentRunStatus: "EXECUTING", | ||
| now: NOW, | ||
| }) | ||
| ).toBe("ACTIVE"); | ||
| }); | ||
|
|
||
| it("returns IDLE when the current run has reached a terminal state", () => { | ||
| expect( | ||
| deriveSessionStatus({ | ||
| closedAt: null, | ||
| expiresAt: null, | ||
| hasCurrentRun: true, | ||
| currentRunStatus: "EXPIRED", | ||
| now: NOW, | ||
| }) | ||
| ).toBe("IDLE"); | ||
| }); | ||
|
|
||
| it("returns IDLE when there is no current run", () => { | ||
| expect( | ||
| deriveSessionStatus({ | ||
| closedAt: null, | ||
| expiresAt: null, | ||
| hasCurrentRun: false, | ||
| currentRunStatus: undefined, | ||
| now: NOW, | ||
| }) | ||
| ).toBe("IDLE"); | ||
| }); | ||
|
|
||
| it("returns IDLE when the current run pointer can't be resolved (status unknown)", () => { | ||
| expect( | ||
| deriveSessionStatus({ | ||
| closedAt: null, | ||
| expiresAt: null, | ||
| hasCurrentRun: true, | ||
| currentRunStatus: undefined, | ||
| now: NOW, | ||
| }) | ||
| ).toBe("IDLE"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { type TaskRunStatus } from "@trigger.dev/database"; | ||
| import { type SessionDisplayStatus } from "~/services/sessionsRepository/sessionsRepository.server"; | ||
| import { isFinalRunStatus } from "~/v3/taskStatus"; | ||
|
|
||
| export type DeriveSessionStatusInput = { | ||
| /** `Session.closedAt` — set once when the session is explicitly closed. */ | ||
| closedAt: Date | null; | ||
| /** `Session.expiresAt` — retention deadline, if any. */ | ||
| expiresAt: Date | null; | ||
| /** Whether the session points at a current run at all. */ | ||
| hasCurrentRun: boolean; | ||
| /** | ||
| * Status of the current run. `undefined` when there is no current run, or the | ||
| * pointer couldn't be resolved (stale / cross-env). | ||
| */ | ||
| currentRunStatus: TaskRunStatus | undefined; | ||
| /** `Date.now()` at the time of derivation. */ | ||
| now: number; | ||
| }; | ||
|
|
||
| /** | ||
| * Derives the display status of a session from its terminal markers and the | ||
| * liveness of its current run. | ||
| * | ||
| * Precedence: an explicit close wins, then an elapsed retention deadline. Only | ||
| * then do we ask whether the session is genuinely live: it's `ACTIVE` when its | ||
| * current run exists and is non-final, otherwise `IDLE` (open but nothing | ||
| * running). This is what stops an abandoned session whose run terminated long | ||
| * ago from reading `ACTIVE` forever. | ||
| */ | ||
| export function deriveSessionStatus(input: DeriveSessionStatusInput): SessionDisplayStatus { | ||
| if (input.closedAt != null) { | ||
| return "CLOSED"; | ||
| } | ||
|
|
||
| if (input.expiresAt != null && input.expiresAt.getTime() < input.now) { | ||
| return "EXPIRED"; | ||
| } | ||
|
|
||
| const hasLiveRun = | ||
| input.hasCurrentRun && | ||
| input.currentRunStatus !== undefined && | ||
| !isFinalRunStatus(input.currentRunStatus); | ||
|
|
||
| return hasLiveRun ? "ACTIVE" : "IDLE"; | ||
| } | ||
|
Comment on lines
+31
to
+46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Run detail page still derives session status the old way, so it can disagree with the new Idle label The run/span page renders a session badge from Was this helpful? React with 👍 or 👎 to provide feedback.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good flag, splitting it in two:
|
||
Uh oh!
There was an error while loading. Please reload this page.