|
1 | 1 | import { json } from "@remix-run/server-runtime"; |
| 2 | +import { tryCatch } from "@trigger.dev/core/utils"; |
2 | 3 | import { CloseSessionRequestBody, type RetrieveSessionResponseBody } from "@trigger.dev/core/v3"; |
| 4 | +import type { Session } from "@trigger.dev/database"; |
3 | 5 | import { z } from "zod"; |
4 | 6 | import { $replica, prisma } from "~/db.server"; |
| 7 | +import { logger } from "~/services/logger.server"; |
| 8 | +import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; |
| 9 | +import { appendServerSessionRecord } from "~/services/realtime/sessionChannelAppend.server"; |
5 | 10 | import { |
| 11 | + canonicalSessionAddressingKey, |
6 | 12 | resolveSessionByIdOrExternalId, |
7 | 13 | serializeSessionWithFriendlyRunId, |
8 | 14 | } from "~/services/realtime/sessions.server"; |
9 | 15 | import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; |
| 16 | +import { runStore } from "~/v3/runStore.server"; |
10 | 17 |
|
11 | 18 | const ParamsSchema = z.object({ |
12 | 19 | session: z.string(), |
@@ -62,8 +69,83 @@ const { action, loader } = createActionApiRoute( |
62 | 69 |
|
63 | 70 | const updated = await prisma.session.findFirst({ where: { id: existing.id } }); |
64 | 71 | if (!updated) return json({ error: "Session not found" }, { status: 404 }); |
| 72 | + |
| 73 | + // Best effort in full: the row is closed and that is what the caller asked |
| 74 | + // for. A failure resolving the calling run or building the stream client |
| 75 | + // must not turn a successful close into a 500. |
| 76 | + const [notifyError] = await tryCatch( |
| 77 | + notifyLiveRunOfClose({ |
| 78 | + session: updated, |
| 79 | + environment: authentication.environment, |
| 80 | + paramSession: params.session, |
| 81 | + callingRunId: body.callingRunId, |
| 82 | + reason: body.reason, |
| 83 | + }) |
| 84 | + ); |
| 85 | + if (notifyError) { |
| 86 | + logger.error("Failed to notify the live run of a session close", { |
| 87 | + sessionId: updated.id, |
| 88 | + error: notifyError, |
| 89 | + }); |
| 90 | + } |
| 91 | + |
65 | 92 | return json<RetrieveSessionResponseBody>(await serializeSessionWithFriendlyRunId(updated)); |
66 | 93 | } |
67 | 94 | ); |
68 | 95 |
|
| 96 | +/** |
| 97 | + * Put a `trigger: "close"` record on the session's `.in` channel so a live run |
| 98 | + * leaves its loop now instead of sitting until its idle timeout. Appending also |
| 99 | + * completes the run's `.in` waitpoint, which is what wakes a suspended run. |
| 100 | + * |
| 101 | + * Skipped when the close came from the session's own run (`chat.close()`), which |
| 102 | + * is already exiting and would never consume the record. Best effort throughout: |
| 103 | + * the row is closed either way, and further appends are refused. |
| 104 | + */ |
| 105 | +async function notifyLiveRunOfClose({ |
| 106 | + session, |
| 107 | + environment, |
| 108 | + paramSession, |
| 109 | + callingRunId, |
| 110 | + reason, |
| 111 | +}: { |
| 112 | + session: Session; |
| 113 | + environment: AuthenticatedEnvironment; |
| 114 | + paramSession: string; |
| 115 | + callingRunId?: string; |
| 116 | + reason?: string; |
| 117 | +}): Promise<void> { |
| 118 | + if (!session.currentRunId) { |
| 119 | + return; |
| 120 | + } |
| 121 | + |
| 122 | + if (callingRunId) { |
| 123 | + const callingRun = await runStore.findRun( |
| 124 | + { friendlyId: callingRunId, runtimeEnvironmentId: environment.id }, |
| 125 | + { select: { id: true } }, |
| 126 | + $replica |
| 127 | + ); |
| 128 | + if (callingRun?.id === session.currentRunId) { |
| 129 | + return; |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + const record = JSON.stringify({ |
| 134 | + kind: "message", |
| 135 | + payload: { |
| 136 | + chatId: session.externalId ?? session.friendlyId, |
| 137 | + trigger: "close", |
| 138 | + ...(reason ? { closedReason: reason } : {}), |
| 139 | + }, |
| 140 | + }); |
| 141 | + |
| 142 | + await appendServerSessionRecord({ |
| 143 | + environment, |
| 144 | + session, |
| 145 | + addressingKey: canonicalSessionAddressingKey(session, paramSession), |
| 146 | + io: "in", |
| 147 | + part: record, |
| 148 | + }); |
| 149 | +} |
| 150 | + |
69 | 151 | export { action, loader }; |
0 commit comments