Files
2026-07-13 13:32:57 +08:00

204 lines
7.4 KiB
TypeScript

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<Awaited<ReturnType<SessionPresenter["call"]>>>;
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,
},
};
}
}