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 .changeset/chat-session-run-ttl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
---

Chat server sessions can now set a `ttl` on the runs they trigger, so a run that is never picked up expires instead of waiting indefinitely.
12 changes: 10 additions & 2 deletions apps/webapp/app/services/dashboardAgent.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,18 @@ export function isDashboardAgentConfigured(): boolean {
return Boolean(env.DASHBOARD_AGENT_SECRET_KEY);
}

// With no agent worker available a turn's run would sit queued indefinitely and
// could be dequeued much later with a stale token. Expire it instead — the turn
// is long dead by then on the client.
const DASHBOARD_AGENT_RUN_TTL = "2m";

// Pins every agent session (and its continuation runs) to a deployed version
// when DASHBOARD_AGENT_VERSION is set; unset runs on the env's current version.
export function dashboardAgentTriggerConfig(): { lockToVersion: string } | undefined {
return env.DASHBOARD_AGENT_VERSION ? { lockToVersion: env.DASHBOARD_AGENT_VERSION } : undefined;
export function dashboardAgentTriggerConfig(): { ttl: string; lockToVersion?: string } {
return {
ttl: DASHBOARD_AGENT_RUN_TTL,
...(env.DASHBOARD_AGENT_VERSION ? { lockToVersion: env.DASHBOARD_AGENT_VERSION } : {}),
};
}

export async function startDashboardAgentSession(params: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ async function triggerSessionRun(params: {
...(config.maxDuration !== undefined ? { maxDuration: config.maxDuration } : {}),
...(config.lockToVersion ? { lockToVersion: config.lockToVersion } : {}),
...(config.region ? { region: config.region } : {}),
...(config.ttl !== undefined ? { ttl: config.ttl } : {}),
},
};

Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/test/realtimeServices.replicaLag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ describe("realtime-svc — replica-lag guards", () => {
environmentType: "DEVELOPMENT",
organizationId: seed.organization.id,
taskIdentifier: "my-task",
triggerConfig: { basePayload: {} },
triggerConfig: { basePayload: {}, ttl: "2m" },
currentRunId: callingRunId,
currentRunVersion: 0,
streamBasinName: "session-pinned-basin",
Expand Down Expand Up @@ -429,6 +429,8 @@ describe("realtime-svc — replica-lag guards", () => {
// previousRunId forwarded to the triggered run is the calling run's cuid (documented fallback).
expect(triggerState.calls).toHaveLength(1);
expect(triggerState.calls[0]!.body.payload.previousRunId).toBe(callingRunId);
// The session's ttl reaches the trigger options, so an undequeued run expires.
expect(triggerState.calls[0]!.body.options.ttl).toBe("2m");
expect(versionCalls.at(-1)).toEqual({ requested: "v2", basin: null });
expect(replica.wasHit("taskRun")).toBe(true);

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/v3/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,11 @@ export const SessionTriggerConfig = z.object({
lockToVersion: z.string().optional(),
/** Region to schedule runs in. Forwarded to `TaskRunOptions.region`. */
region: z.string().optional(),
/**
* How long a run may sit undequeued before it expires (duration string
* like `"2m"`, or seconds). Forwarded to `TaskRunOptions.ttl`.
*/
ttl: z.string().or(z.number().nonnegative().int()).optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 '\bTaskRunOptions\b|\bttl\b' \
  packages/core/src \
  packages/trigger-sdk/src \
  apps/webapp/app/services \
  --glob '*.ts' \
  --glob '*.tsx'

Repository: triggerdotdev/trigger.dev

Length of output: 50383


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- TaskRunOptions and duration-related definitions ---'
rg -n -C 12 '\bTaskRunOptions\b|parseDuration|duration.*string|ttl.*duration|ttl.*seconds' \
  packages/core packages/trigger-sdk apps \
  --glob '*.ts' --glob '*.tsx' \
  | head -n 500

printf '%s\n' '--- Relevant repository conventions and learnings ---'
find /tmp/coderabbit-repo-knowledge/triggerdotdev-trigger-dev-0bdd0019 \
  -type f -name '*.md' -print \
  | sort \
  | while read -r f; do
      case "$f" in
        */learnings/*|*/**/*) ;;
      esac
      if grep -qE 'packages/core|schemas|api.ts' "$f"; then
        printf '\n### %s\n' "$f"
        cat "$f"
      fi
    done

Repository: triggerdotdev/trigger.dev

Length of output: 43183


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- TaskRunOptions declarations and references ---'
rg -n -C 15 \
  '(^|[[:space:]])(export[[:space:]]+)?(type|interface)[[:space:]]+TaskRunOptions\b|\bTaskRunOptions\s*=' \
  . --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx' \
  | head -n 300

printf '%s\n' '--- TTL consumers ---'
rg -n -C 12 \
  'parseNaturalLanguageDurationInMs|parseNaturalLanguageDuration\(|safeParseNaturalLanguageDuration|ttl\s*[/?:=]|\.ttl\b' \
  packages apps --glob '*.ts' --glob '*.tsx' \
  | head -n 700

Repository: triggerdotdev/trigger.dev

Length of output: 214


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- TaskRunOptions exact matches ---'
rg -n -C 10 'TaskRunOptions' . --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx' || true

printf '%s\n' '--- TTL parser and consumer matches ---'
rg -n -C 10 \
  'parseNaturalLanguageDurationInMs|parseNaturalLanguageDuration\(|safeParseNaturalLanguageDuration|ttl\s*[/?:=]|\.ttl\b' \
  packages apps --glob '*.ts' --glob '*.tsx' \
  | head -n 700 || true

Repository: triggerdotdev/trigger.dev

Length of output: 50383


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- SessionTriggerConfig consumers ---'
rg -n -C 14 'SessionTriggerConfig|triggerConfig\.ttl|ttl:.*triggerConfig|triggerConfig.*ttl' \
  . --glob '*.ts' --glob '*.tsx' \
  | head -n 500 || true

printf '%s\n' '--- TTL handling outside the core schemas ---'
rg -n -C 12 \
  'parseNaturalLanguageDurationInMs|parseNaturalLanguageDurationAgo|parseNaturalLanguageDuration|ttlSeconds|ttlInSeconds|ttl.*run|run.*ttl' \
  packages apps --glob '*.ts' --glob '*.tsx' \
  | grep -vE '(^|/)(.*test|.*spec)\.' \
  | head -n 700 || true

Repository: triggerdotdev/trigger.dev

Length of output: 50383


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- Session-related source files ---'
git ls-files \
  | grep -Ei '(^|/)(session|sessions)([^/]*)(/|\.|$)|session' \
  | head -n 250 || true

printf '%s\n' '--- Exact request-schema usage ---'
rg -n -C 12 'CreateSessionRequestBody|SessionTriggerConfig' \
  apps packages --glob '*.ts' --glob '*.tsx' \
  | grep -v 'packages/core/src/v3/schemas/api.ts' \
  | head -n 500 || true

Repository: triggerdotdev/trigger.dev

Length of output: 48007


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- Session create route ---'
sed -n '1,260p' 'apps/webapp/app/routes/api.v1.sessions.ts' 2>/dev/null || true

printf '%s\n' '--- TTL references in the session and run-trigger path ---'
rg -n -C 10 '\bttl\b|CreateSessionRequestBody|TaskOptions|TaskRunOptions' \
  'apps/webapp/app/routes/api.v1.sessions.ts' \
  'apps/webapp/app/services' \
  'internal-packages/run-store' \
  'packages/core/src/v3/runEngineWorker' \
  'packages/trigger-sdk/src/v3/sessions.ts' \
  | head -n 700 || true

printf '%s\n' '--- Change description for session run TTL ---'
cat .changeset/chat-session-run-ttl.md 2>/dev/null || true

Repository: triggerdotdev/trigger.dev

Length of output: 50381


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- Session service outlines ---'
ast-grep outline apps/webapp/app/services/realtime/sessionRunManager.server.ts 2>/dev/null || true
ast-grep outline apps/webapp/app/services/realtime/sessions.server.ts 2>/dev/null || true

printf '%s\n' '--- Session persistence and run-trigger implementations ---'
rg -n -C 18 \
  'export async function findOrCreateSession|function findOrCreateSession|export async function ensureRunForSession|function ensureRunForSession|triggerConfig|ttl' \
  apps/webapp/app/services/realtime/sessionRunManager.server.ts \
  apps/webapp/app/services/realtime/sessions.server.ts \
  | head -n 900 || true

Repository: triggerdotdev/trigger.dev

Length of output: 46993


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- TriggerTaskService contract and option handling ---'
rg -n -C 20 \
  'class TriggerTaskService|export class TriggerTaskService|async call\(|options\.ttl|ttl:|TaskOptions|TaskRunOptions' \
  apps/webapp/app/v3/services/triggerTask.server.ts \
  apps/webapp/app/v3/services \
  apps/webapp/app --glob '*.ts' --glob '*.tsx' \
  | head -n 900 || true

Repository: triggerdotdev/trigger.dev

Length of output: 50381


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- TriggerTaskService TTL flow ---'
rg -n -C 25 '\bttl\b|TriggerTaskRequestBody|createRun|createTaskRun|triggerRun' \
  apps/webapp/app/v3/services/triggerTask.server.ts \
  | head -n 500 || true

printf '%s\n' '--- TriggerTaskRequestBody definition ---'
rg -n -C 20 'TriggerTaskRequestBody' packages/core/src/v3/schemas/api.ts packages/core/src/v3 \
  | head -n 350 || true

Repository: triggerdotdev/trigger.dev

Length of output: 34835


🌐 Web query:

Trigger.dev TaskRunOptions ttl duration string implementation

💡 Result:

In Trigger.dev, the ttl (Time-to-Live) option for task runs accepts either a duration string or a number of seconds [1][2][3]. Duration String Format The duration string follows a flexible format that combines numbers and units, such as "1h", "30m", "90s", or "1h42m" [2][3]. This string represents the maximum time a run is allowed to remain in a queued state before it is automatically marked as "Expired" and prevented from executing [1][4][5]. Numeric Format Alternatively, you can provide a number, which is interpreted as the duration in seconds (with a minimum of 1 second) [1][2][5]. Usage and Precedence You can configure the TTL at three distinct levels, with the following order of precedence (from highest to lowest) [4]: 1. Per-trigger: Passed within the options object when triggering a task [1][4][5]. await myTask.trigger({ payload }, { ttl: "1h" }); // Duration string await myTask.trigger({ payload }, { ttl: 3600 }); // Seconds 2. Task-level default: Defined within the task's configuration [4][5]. export const myTask = task({ id: "my-task", ttl: "10m", run: async (payload) => {... }, }); 3. Global config default: Set in your trigger.config.ts file [4]. To opt out of a config-level or task-level TTL for a specific execution, you can set ttl: 0 [4][5]. On Trigger.dev Cloud, all runs are subject to a maximum TTL of 14 days; if you specify a longer TTL, it will be automatically clamped to this limit [1][4][6]. If no TTL is explicitly configured, runs default to this 14-day limit in production environments [1][4][6]. Note that in development, the default TTL is typically 10 minutes unless otherwise configured [1][4].

Citations:


Reject invalid SessionTriggerConfig.ttl values.

z.string() allows "" and "not-a-duration" to pass CreateSessionRequestBody. The session stores these values and forwards them as TriggerTaskService options.ttl. Validate the string branch against the duration grammar while preserving 0 as the disabled-TTL value.

/** Convenience field surfaced to chat.agent via the wire payload. */
idleTimeoutInSeconds: z.number().int().positive().max(3600).optional(),
});
Expand Down
2 changes: 2 additions & 0 deletions packages/trigger-sdk/src/v3/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10448,6 +10448,7 @@ function createChatStartSessionAction<TChat extends AnyTask = AnyTask>(
const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
const idleTimeoutInSeconds =
params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
const ttl = params.triggerConfig?.ttl ?? options?.triggerConfig?.ttl;

const triggerConfig: SessionTriggerConfig = {
basePayload: {
Expand All @@ -10470,6 +10471,7 @@ function createChatStartSessionAction<TChat extends AnyTask = AnyTask>(
...(options?.triggerConfig?.region || params.triggerConfig?.region
? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region }
: {}),
...(ttl !== undefined ? { ttl } : {}),
...(options?.triggerConfig?.lockToVersion || params.triggerConfig?.lockToVersion
? {
lockToVersion:
Expand Down
4 changes: 3 additions & 1 deletion packages/trigger-sdk/src/v3/chat-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ describe("chat.headStart (route handler)", () => {
expect(body.triggerConfig.basePayload.idleTimeoutInSeconds).toBe(60);
});

it("merges triggerConfig tags and queue into createSession", async () => {
it("merges triggerConfig tags, queue and ttl into createSession", async () => {
const requests: CapturedRequest[] = [];
global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
const urlStr = typeof url === "string" ? url : url.toString();
Expand Down Expand Up @@ -248,6 +248,7 @@ describe("chat.headStart (route handler)", () => {
triggerConfig: {
tags: ["org:acme", "agentic-run:xyz"],
queue: "my-queue",
ttl: "2m",
},
run: async ({ chat: chatHelper }) => {
return streamText({
Expand Down Expand Up @@ -276,6 +277,7 @@ describe("chat.headStart (route handler)", () => {
const body = JSON.parse(sessionCreate!.init!.body as string);
expect(body.triggerConfig.tags).toEqual(["chat:chat-1", "org:acme", "agentic-run:xyz"]);
expect(body.triggerConfig.queue).toBe("my-queue");
expect(body.triggerConfig.ttl).toBe("2m");
expect(body.triggerConfig.basePayload.trigger).toBe("handover-prepare");
expect(body.triggerConfig.basePayload.chatId).toBe("chat-1");
});
Expand Down
1 change: 1 addition & 0 deletions packages/trigger-sdk/src/v3/chat-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ async function openHandoverSession(opts: {
? { maxDuration: opts.triggerConfig.maxDuration }
: {}),
...(opts.triggerConfig?.region ? { region: opts.triggerConfig.region } : {}),
...(opts.triggerConfig?.ttl !== undefined ? { ttl: opts.triggerConfig.ttl } : {}),
...(opts.triggerConfig?.lockToVersion
? { lockToVersion: opts.triggerConfig.lockToVersion }
: {}),
Expand Down
13 changes: 12 additions & 1 deletion packages/trigger-sdk/src/v3/createStartSessionAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,21 +115,32 @@ describe("chat.createStartSessionAction — runtime", () => {
]);
});

it("forwards maxDuration, region, and lockToVersion from triggerConfig", async () => {
it("forwards maxDuration, region, lockToVersion, and ttl from triggerConfig", async () => {
installStartFixture();

const start = chat.createStartSessionAction("fake-chat", {
triggerConfig: {
maxDuration: 120,
region: "us-east-1",
lockToVersion: "20260101.1",
ttl: "2m",
},
});
await start({ chatId: "chat-parity" });

expect(lastStartBody?.triggerConfig.maxDuration).toBe(120);
expect(lastStartBody?.triggerConfig.region).toBe("us-east-1");
expect(lastStartBody?.triggerConfig.lockToVersion).toBe("20260101.1");
expect(lastStartBody?.triggerConfig.ttl).toBe("2m");
});

it("omits ttl when triggerConfig does not set it", async () => {
installStartFixture();

const start = chat.createStartSessionAction("fake-chat");
await start({ chatId: "chat-no-ttl" });

expect(lastStartBody?.triggerConfig).not.toHaveProperty("ttl");
});

it("server-mints override tokens for additional API keys", async () => {
Expand Down
Loading