import { type Span } from "@opentelemetry/api"; import { type PrismaClientOrTransaction } from "@trigger.dev/database"; 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"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { startActiveSpan } from "~/v3/tracer.server"; export type SessionDetail = NonNullable>>; export class SessionPresenter { constructor(private readonly replica: PrismaClientOrTransaction) {} public async call(args: { userId: string; environmentId: string; sessionParam: string; projectExternalRef: string; environmentSlug: string; }) { return startActiveSpan("SessionPresenter.call", (span) => this.#call(args, span), { attributes: { environmentId: args.environmentId, sessionParam: args.sessionParam, }, }); } async #call( { userId, environmentId, sessionParam, projectExternalRef, environmentSlug, }: { userId: string; environmentId: string; sessionParam: string; projectExternalRef: string; environmentSlug: string; }, rootSpan: Span ) { const session = await startActiveSpan("SessionPresenter.resolveSession", () => resolveSessionByIdOrExternalId(this.replica, environmentId, sessionParam) ); if (!session) { rootSpan.setAttribute("session.found", false); return null; } rootSpan.setAttribute("session.found", true); rootSpan.setAttribute("session.id", session.id); const displayableEnvironment = await startActiveSpan( "SessionPresenter.findDisplayableEnvironment", () => findDisplayableEnvironment(environmentId, userId) ); if (!displayableEnvironment) { throw new ServiceValidationError("No environment found"); } // Run history is append-only; latest first matches the runs list. // 50 covers the vast majority of sessions; longer histories link out // to the runs page via tag filter. const sessionRuns = await startActiveSpan("SessionPresenter.findSessionRuns", async (span) => { const rows = await this.replica.sessionRun.findMany({ where: { sessionId: session.id }, orderBy: { triggeredAt: "desc" }, take: 50, select: { id: true, runId: true, reason: true, triggeredAt: true, }, }); span.setAttribute("sessionRuns.count", rows.length); return rows; }); const runIds = sessionRuns.map((r) => r.runId); const runs = await startActiveSpan("SessionPresenter.findRuns", async (span) => { span.setAttribute("runIds.count", runIds.length); return runIds.length > 0 ? runStore.findRuns( { where: { id: { in: runIds } }, select: { id: true, friendlyId: true, status: true }, }, this.replica ) : []; }); const runsById = new Map(runs.map((r) => [r.id, r] as const)); const currentRun = session.currentRunId ? (runsById.get(session.currentRunId) ?? (await startActiveSpan("SessionPresenter.findCurrentRunFallback", () => runStore.findRun( { id: session.currentRunId! }, { select: { id: true, friendlyId: true, status: true }, }, this.replica ) ))) : null; // The dashboard SSE route is cookie-authed, so `publicAccessToken` is // unused — kept here to match the existing `AgentViewAuth` shape. const addressingKey = session.externalId ?? session.friendlyId; // Presign a GET URL for the agent's S3 snapshot blob. The browser // fetches it directly, parses + validates, and seeds the // TriggerChatTransport with the full history + lastEventId before // opening the SSE. Presign succeeds regardless of whether the blob // exists; the frontend handles 404 gracefully. // // Snapshots are only written when no `hydrateMessages` hook is // registered — sessions that use `hydrateMessages` will 404 here // and the dashboard falls back to seq=0 SSE (which, post-trim, // shows only the most recent turn — accepted, those customers // have their own DB-backed dashboards). // Resolve the snapshot key via the SAME helper the SDK write + boot read // use (`chatSnapshotStorageKey`), so the dashboard GET hits the exact // object (and object store) the snapshot was written to. Recomputing a // bare key here was the bug: an unqualified key reads the base store while // the write applied OBJECT_STORE_DEFAULT_PROTOCOL, so they could diverge. let snapshotPresignedUrl: string | undefined; try { const signed = await startActiveSpan("SessionPresenter.presignSnapshot", async () => generatePresignedUrl( projectExternalRef, environmentSlug, chatSnapshotStorageKey(session), "GET" ) ); if (signed.success) { snapshotPresignedUrl = signed.url; } else { logger.warn("SessionPresenter: snapshot presign failed", { sessionId: session.id, error: signed.error, }); } } catch (error) { logger.warn("SessionPresenter: snapshot presign threw", { sessionId: session.id, error: error instanceof Error ? error.message : String(error), }); } return { id: session.id, friendlyId: session.friendlyId, externalId: session.externalId, type: session.type, taskIdentifier: session.taskIdentifier, 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, closedAt: session.closedAt ? session.closedAt.toISOString() : undefined, closedReason: session.closedReason ?? undefined, expiresAt: session.expiresAt ? session.expiresAt.toISOString() : undefined, createdAt: session.createdAt.toISOString(), updatedAt: session.updatedAt.toISOString(), environment: displayableEnvironment, currentRun: currentRun ? { friendlyId: currentRun.friendlyId, status: currentRun.status } : null, runs: sessionRuns.map((r) => { const run = runsById.get(r.runId); return { id: r.id, reason: r.reason, triggeredAt: r.triggeredAt.toISOString(), run: run ? { friendlyId: run.friendlyId, status: run.status } : null, }; }), agentView: { publicAccessToken: "", apiOrigin: env.API_ORIGIN || env.LOGIN_ORIGIN, sessionId: addressingKey, initialMessages: [], snapshotPresignedUrl, }, }; } }