@@ -168,7 +183,7 @@ export function SessionsTable({
)}
{isLoading && (
Loading…
diff --git a/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts b/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts
index bff1bda0177..a5bcd37744b 100644
--- a/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts
@@ -7,6 +7,7 @@ import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
import {
type SessionStatus,
SessionsRepository,
+ LEGACY_PLAYGROUND_TAG,
} from "~/services/sessionsRepository/sessionsRepository.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
@@ -225,7 +226,13 @@ export class SessionListPresenter {
externalId: session.externalId,
type: session.type,
taskIdentifier: session.taskIdentifier,
- tags: session.tags ? [...session.tags].sort((a, b) => a.localeCompare(b)) : [],
+ isTest: session.isTest,
+ // Hide the legacy "playground" tag (pre-isTest sessions) from display.
+ tags: session.tags
+ ? [...session.tags]
+ .filter((t) => t !== LEGACY_PLAYGROUND_TAG)
+ .sort((a, b) => a.localeCompare(b))
+ : [],
status,
closedAt: session.closedAt ? session.closedAt.toISOString() : undefined,
closedReason: session.closedReason ?? undefined,
diff --git a/apps/webapp/app/presenters/v3/SessionPresenter.server.ts b/apps/webapp/app/presenters/v3/SessionPresenter.server.ts
index 36ef46d4b4e..7b4183426c8 100644
--- a/apps/webapp/app/presenters/v3/SessionPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/SessionPresenter.server.ts
@@ -4,6 +4,7 @@ import { env } from "~/env.server";
import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
import { chatSnapshotStorageKey } from "~/services/realtime/chatSnapshot.server";
import { resolveSessionByIdOrExternalId } from "~/services/realtime/sessions.server";
+import { LEGACY_PLAYGROUND_TAG } from "~/services/sessionsRepository/sessionsRepository.server";
import { logger } from "~/services/logger.server";
import { generatePresignedUrl } from "~/v3/objectStore.server";
import { runStore } from "~/v3/runStore.server";
@@ -177,7 +178,13 @@ export class SessionPresenter {
externalId: session.externalId,
type: session.type,
taskIdentifier: session.taskIdentifier,
- tags: session.tags ? [...session.tags].sort((a, b) => a.localeCompare(b)) : [],
+ isTest: session.isTest,
+ // Hide the legacy "playground" tag (pre-isTest sessions) from display.
+ tags: session.tags
+ ? [...session.tags]
+ .filter((t) => t !== LEGACY_PLAYGROUND_TAG)
+ .sort((a, b) => a.localeCompare(b))
+ : [],
metadata: session.metadata,
triggerConfig: session.triggerConfig,
streamBasinName: session.streamBasinName,
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx
index 2bf62b8cad2..b9bd387159b 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx
@@ -1,5 +1,5 @@
import { BoltIcon, BoltSlashIcon } from "@heroicons/react/20/solid";
-import { BookOpenIcon } from "@heroicons/react/24/solid";
+import { BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
import { type MetaFunction } from "@remix-run/react";
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { useVirtualizer } from "@tanstack/react-virtual";
@@ -818,6 +818,19 @@ function OverviewTab({ session, status }: { session: LoadedSession; status: Sess
{session.taskIdentifier}
+
+ Test
+
+ {session.isTest ? "Yes" : "No"}
+ {session.isTest ? (
+
+ ) : (
+
+ –
+
+ )}
+
+
{session.currentRun ? (
Current run
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx
index da77d2cc692..826618277af 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx
@@ -162,7 +162,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
type: "chat.agent",
taskIdentifier: agentSlug,
triggerConfig: triggerConfig as unknown as Prisma.InputJsonValue,
- tags: ["playground"],
+ // Mark as a Test session — surfaced via the Test column in the
+ // Sessions table. Session tags stay empty; the triggered run still
+ // carries "playground:true" via triggerConfig.tags.
+ isTest: true,
projectId: project.id,
runtimeEnvironmentId: environment.id,
environmentType: environment.type,
diff --git a/apps/webapp/app/services/sessionsReplicationService.server.ts b/apps/webapp/app/services/sessionsReplicationService.server.ts
index 12b66e29dfb..da202458ca8 100644
--- a/apps/webapp/app/services/sessionsReplicationService.server.ts
+++ b/apps/webapp/app/services/sessionsReplicationService.server.ts
@@ -802,6 +802,7 @@ function toSessionInsertArray(
session.expiresAt ? session.expiresAt.getTime() : null,
session.createdAt.getTime(),
session.updatedAt.getTime(),
+ session.isTest ?? false,
version.toString(),
isDeleted ? 1 : 0,
];
diff --git a/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts b/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts
index aebf61628fa..b66faf4d3e0 100644
--- a/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts
+++ b/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts
@@ -93,6 +93,7 @@ export class ClickHouseSessionsRepository implements ISessionsRepository {
externalId: true,
type: true,
taskIdentifier: true,
+ isTest: true,
tags: true,
metadata: true,
closedAt: true,
diff --git a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts
index 245f1df2295..5a808051035 100644
--- a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts
+++ b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts
@@ -24,6 +24,13 @@ export type SessionsRepositoryOptions = {
export const SessionStatus = z.enum(["ACTIVE", "CLOSED", "EXPIRED"]);
export type SessionStatus = z.infer;
+/**
+ * Legacy marker tag for sessions created from the Test/playground before the
+ * `Session.isTest` boolean existed. New sessions set `isTest` instead; this tag
+ * is hidden from the Tags display so it doesn't surface on pre-isTest rows.
+ */
+export const LEGACY_PLAYGROUND_TAG = "playground";
+
const SessionListInputOptionsSchema = z.object({
organizationId: z.string(),
projectId: z.string(),
@@ -87,6 +94,7 @@ export type ListedSession = Prisma.SessionGetPayload<{
externalId: true;
type: true;
taskIdentifier: true;
+ isTest: true;
tags: true;
metadata: true;
closedAt: true;
diff --git a/apps/webapp/test/sessionsReplicationService.test.ts b/apps/webapp/test/sessionsReplicationService.test.ts
index 1d3c761e813..cb171ed5b54 100644
--- a/apps/webapp/test/sessionsReplicationService.test.ts
+++ b/apps/webapp/test/sessionsReplicationService.test.ts
@@ -80,6 +80,7 @@ describe("SessionsReplicationService", () => {
},
tags: ["user:42", "plan:pro"],
metadata: { plan: "pro", seats: 3 },
+ isTest: true,
},
});
@@ -108,6 +109,7 @@ describe("SessionsReplicationService", () => {
environment_type: "DEVELOPMENT",
task_identifier: "my-agent",
tags: ["user:42", "plan:pro"],
+ is_test: 1,
_is_deleted: 0,
})
);
diff --git a/internal-packages/clickhouse/schema/034_add_is_test_to_sessions_v1.sql b/internal-packages/clickhouse/schema/034_add_is_test_to_sessions_v1.sql
new file mode 100644
index 00000000000..8d2f46e1249
--- /dev/null
+++ b/internal-packages/clickhouse/schema/034_add_is_test_to_sessions_v1.sql
@@ -0,0 +1,11 @@
+-- +goose Up
+-- Existing rows default to 0 and are intentionally NOT backfilled: the Sessions
+-- list reads isTest from Postgres (ClickHouse only supplies session IDs), so the
+-- UI is correct without it. A backfill is only needed if a ClickHouse-side
+-- isTest filter/aggregate over sessions_v1 is added later.
+ALTER TABLE trigger_dev.sessions_v1
+ ADD COLUMN IF NOT EXISTS is_test UInt8 DEFAULT 0;
+
+-- +goose Down
+ALTER TABLE trigger_dev.sessions_v1
+ DROP COLUMN IF EXISTS is_test;
diff --git a/internal-packages/clickhouse/src/sessions.ts b/internal-packages/clickhouse/src/sessions.ts
index 567fe65511e..995e98447e0 100644
--- a/internal-packages/clickhouse/src/sessions.ts
+++ b/internal-packages/clickhouse/src/sessions.ts
@@ -19,6 +19,7 @@ export const SessionV1 = z.object({
expires_at: z.number().int().nullish(),
created_at: z.number().int(),
updated_at: z.number().int(),
+ is_test: z.boolean().default(false),
_version: z.string(),
_is_deleted: z.number().int().default(0),
});
@@ -43,6 +44,7 @@ export const SESSION_COLUMNS = [
"expires_at",
"created_at",
"updated_at",
+ "is_test",
"_version",
"_is_deleted",
] as const;
@@ -70,6 +72,7 @@ export type SessionFieldTypes = {
expires_at: number | null;
created_at: number;
updated_at: number;
+ is_test: boolean;
_version: string;
_is_deleted: number;
};
@@ -95,6 +98,7 @@ export type SessionInsertArray = [
expires_at: number | null,
created_at: number,
updated_at: number,
+ is_test: boolean,
_version: string,
_is_deleted: number,
];
diff --git a/internal-packages/database/prisma/migrations/20260621000000_add_is_test_to_session/migration.sql b/internal-packages/database/prisma/migrations/20260621000000_add_is_test_to_session/migration.sql
new file mode 100644
index 00000000000..5e50a167f21
--- /dev/null
+++ b/internal-packages/database/prisma/migrations/20260621000000_add_is_test_to_session/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Session" ADD COLUMN "isTest" BOOLEAN NOT NULL DEFAULT false;
diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma
index bb80da3a7ec..9658e664347 100644
--- a/internal-packages/database/prisma/schema.prisma
+++ b/internal-packages/database/prisma/schema.prisma
@@ -805,6 +805,10 @@ model Session {
/// (chatId, messages, trigger) are merged at trigger time.
triggerConfig Json
+ /// Whether this session was created from the Test/playground UI rather
+ /// than by a real chat.agent() trigger. Mirrors TaskRun.isTest.
+ isTest Boolean @default(false)
+
tags String[] @default([])
metadata Json?