Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/sessions-frozen-duration.md
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 ever-growing duration for a session whose run finished long ago. The duration now stops at the last run's activity, and only sessions with a run still executing keep counting up.
29 changes: 22 additions & 7 deletions apps/webapp/app/components/sessions/v1/SessionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,23 +195,38 @@ export function SessionsTable({
}

function SessionDuration({ session }: { session: SessionListItem }) {
// Active sessions tick live; closed/expired sessions freeze at the
// moment they ended (closedAt for explicit closes, expiresAt when the
// TTL ran out without a close call).
const endedAt =
// Closed and expired sessions freeze at the moment they ended.
const terminalEnd =
session.status === "CLOSED"
? session.closedAt
: session.status === "EXPIRED"
? session.expiresAt
: undefined;

if (endedAt) {
if (terminalEnd) {
return (
<>{formatDuration(new Date(session.createdAt), new Date(endedAt), { style: "short" })}</>
<>{formatDuration(new Date(session.createdAt), new Date(terminalEnd), { style: "short" })}</>
);
}

return <LiveTimer startTime={new Date(session.createdAt)} />;
// An open session ticks only while a run is genuinely executing; otherwise it
// freezes at the last run's completion so the duration doesn't climb forever.
if (session.hasLiveRun) {
return <LiveTimer startTime={new Date(session.createdAt)} />;
}

if (session.currentRunCompletedAt) {
return (
<>
{formatDuration(new Date(session.createdAt), new Date(session.currentRunCompletedAt), {
style: "short",
})}
</>
);
}

// Open session that never ran — nothing to measure.
return <span className="text-text-dimmed">–</span>;
}

function SessionActionsCell({ runPath, allRunsPath }: { runPath?: string; allRunsPath: string }) {
Expand Down
18 changes: 16 additions & 2 deletions apps/webapp/app/presenters/v3/SessionListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
LEGACY_PLAYGROUND_TAG,
} from "~/services/sessionsRepository/sessionsRepository.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { isSessionLive } from "./isSessionLive";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
import { runStore } from "~/v3/runStore.server";
import { startActiveSpan } from "~/v3/tracer.server";
Expand Down Expand Up @@ -195,7 +196,7 @@ export class SessionListPresenter {
projectId,
runtimeEnvironmentId: environmentId,
},
select: { id: true, friendlyId: true },
select: { id: true, friendlyId: true, status: true, completedAt: true },
},
this.replica
)
Expand All @@ -208,14 +209,21 @@ export class SessionListPresenter {

return {
sessions: sessions.map((session) => {
const currentRun = session.currentRunId ? runById.get(session.currentRunId) : undefined;

const status: SessionStatus =
session.closedAt != null
? "CLOSED"
: session.expiresAt != null && session.expiresAt.getTime() < now
? "EXPIRED"
: "ACTIVE";

const currentRun = session.currentRunId ? runById.get(session.currentRunId) : undefined;
// Whether a run is genuinely executing right now. Drives the duration
// cell (tick vs freeze); it does NOT affect the filterable status.
const hasLiveRun = isSessionLive({
hasCurrentRun: session.currentRunId != null,
currentRunStatus: currentRun?.status,
});
Comment thread
D-K-P marked this conversation as resolved.

return {
id: session.id,
Expand All @@ -238,6 +246,12 @@ export class SessionListPresenter {
updatedAt: session.updatedAt.toISOString(),
environment: displayableEnvironment,
currentRunFriendlyId: currentRun?.friendlyId,
hasLiveRun,
// Freeze point for the duration when the session isn't live: when its
// current run finished. Undefined when it never ran (renders a dash).
currentRunCompletedAt: currentRun?.completedAt
? currentRun.completedAt.toISOString()
: undefined,
};
}),
pagination: {
Expand Down
24 changes: 24 additions & 0 deletions apps/webapp/app/presenters/v3/isSessionLive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { isSessionLive } from "./isSessionLive";

describe("isSessionLive", () => {
it("is live when the current run is executing", () => {
expect(isSessionLive({ hasCurrentRun: true, currentRunStatus: "EXECUTING" })).toBe(true);
});

it("treats any non-final run status as live", () => {
expect(isSessionLive({ hasCurrentRun: true, currentRunStatus: "PENDING" })).toBe(true);
});

it("is not live when the current run has reached a terminal state", () => {
expect(isSessionLive({ hasCurrentRun: true, currentRunStatus: "EXPIRED" })).toBe(false);
});

it("is not live when there is no current run", () => {
expect(isSessionLive({ hasCurrentRun: false, currentRunStatus: undefined })).toBe(false);
});

it("is not live when the current run pointer can't be resolved (status unknown)", () => {
expect(isSessionLive({ hasCurrentRun: true, currentRunStatus: undefined })).toBe(false);
});
});
22 changes: 22 additions & 0 deletions apps/webapp/app/presenters/v3/isSessionLive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type TaskRunStatus } from "@trigger.dev/database";
import { isFinalRunStatus } from "~/v3/taskStatus";

export type IsSessionLiveInput = {
/** 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;
};

/**
* A session is "live" when its current run is still executing (a non-final run
* status). This drives whether the session's duration ticks or freezes; it does
* NOT change the session's status, which stays the filterable
* `ACTIVE`/`CLOSED`/`EXPIRED` set derived from `closedAt`/`expiresAt`.
*/
export function isSessionLive({ hasCurrentRun, currentRunStatus }: IsSessionLiveInput): boolean {
return hasCurrentRun && currentRunStatus !== undefined && !isFinalRunStatus(currentRunStatus);
}
Loading