fix(core,sdk,webapp): allow 10 session trigger tags, matching the run tag limit (#4832)

## Summary

`SessionTriggerConfig.tags` was capped at 5, while runs (and the [tags
docs](https://trigger.dev/docs/tags)) allow 10. Session trigger tags are
forwarded verbatim as the run tags on every run a session schedules, so
the lower cap was an inconsistency rather than a separate limit. For
`chat.agent` it was worse in practice: the SDK prepends `chat:{chatId}`
automatically and truncates, so users could only get 4 of their own tags
through.

The schema, the SDK truncation points, and the dashboard playground now
all use 10. `chat.agent` users get 9 of their own tags plus the
automatic `chat:{chatId}` tag. Docs updated to say so.
This commit is contained in:
Matt Aitken
2026-08-29 18:57:25 +01:00
committed by GitHub
parent 054bb32249
commit 9cb5028ce1
10 changed files with 59 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
---
Session `triggerConfig.tags` now accepts up to 10 tags, matching the run tag limit. Previously it was capped at 5, which for `chat.agent` left room for only 4 of your own tags after the automatic `chat:{chatId}` tag.
@@ -123,7 +123,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
.map((t) => t.trim())
.filter(Boolean)
: []),
].slice(0, 5);
].slice(0, 10);
const triggerConfig = {
basePayload: {
+1 -1
View File
@@ -490,7 +490,7 @@ Options for [`chat.headStart()`](/ai-chat/fast-starts#head-start), the warm-serv
| `agentId` | `string` | required | The `chat.agent` / `chat.customAgent` id to hand off to |
| `run` | `(args: HeadStartRunArgs) => Promise<StreamTextResult>` | required | First-turn callback. Call `streamText` and spread `chat.toStreamTextOptions({ tools })` |
| `idleTimeoutInSeconds` | `number` | `60` | How long the agent waits for the handover signal |
| `triggerConfig` | `Partial<SessionTriggerConfig>` | `undefined` | Run options (tags, queue, machine, maxAttempts, maxDuration, region, lockToVersion) for the auto-triggered handover-prepare run. The `chat:{chatId}` tag is prepended automatically |
| `triggerConfig` | `Partial<SessionTriggerConfig>` | `undefined` | Run options (tags, queue, machine, maxAttempts, maxDuration, region, lockToVersion) for the auto-triggered handover-prepare run. The `chat:{chatId}` tag is prepended automatically and counts toward the 10-tag limit |
`chat.headStart(options)` returns the handler `(req: Request) => Promise<Response>`. The `run` callback receives `HeadStartRunArgs`: `{ messages: UIMessage[], signal: AbortSignal, chat: HeadStartChatHelper }`, where the helper exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch. See [Head Start](/ai-chat/fast-starts#head-start) for the full guide.
+1 -1
View File
@@ -111,7 +111,7 @@ const { id, runId, publicAccessToken, isCached } = await sessions.start({
| `type` | `string` | Free-form discriminator. `chat.agent` uses `"chat.agent"`. |
| `externalId` | `string?` | Your stable identity. Cannot start with `session_` (reserved). |
| `taskIdentifier` | `string` | Task this session triggers runs against. |
| `triggerConfig` | `SessionTriggerConfig` | Trigger options applied to every run: `tags`, `queue`, `machine`, `maxAttempts`, `idleTimeoutInSeconds`, `basePayload`. |
| `triggerConfig` | `SessionTriggerConfig` | Trigger options applied to every run: `tags` (up to 10, same as [run tags](/tags); the chat helpers such as `chat.createStartSessionAction` and `AgentChat` add a `chat:{chatId}` tag themselves, which uses one slot. Direct `sessions.start` callers get all 10 and must add any chat tag themselves), `queue`, `machine`, `maxAttempts`, `idleTimeoutInSeconds`, `basePayload`. |
| `tags` | `string[]?` | Up to 10 tags on the Session row (separate from `triggerConfig.tags`). |
| `metadata` | `Record<string, unknown>?` | Arbitrary JSON. |
| `expiresAt` | `Date?` | Hard retention deadline. |
+1 -1
View File
@@ -1852,7 +1852,7 @@ export const SessionTriggerConfig = z.object({
basePayload: z.record(z.unknown()),
machine: MachinePresetName.optional(),
queue: z.string().max(128).optional(),
tags: z.array(z.string().max(128)).max(5).optional(),
tags: z.array(z.string().max(128)).max(10).optional(),
maxAttempts: z.number().int().positive().max(10).optional(),
/** Per-run wall-clock cap (seconds). Forwarded to `TaskRunOptions.maxDuration`. */
maxDuration: z.number().int().positive().optional(),
+16
View File
@@ -222,3 +222,19 @@ export function slimSubmitMessageForWire<TMsg extends UIMessage | undefined>(mes
parts: slimParts,
} as unknown as TMsg;
}
/** Run tags accept at most 10 entries of at most 128 characters each. */
const MAX_RUN_TAGS = 10;
const MAX_RUN_TAG_LENGTH = 128;
/**
* Build the run tags for a chat session: the automatic `chat:{chatId}` tag
* followed by the caller's tags, capped at the run tag limit. The chat tag
* is skipped when the chat ID is too long to fit within a single tag, so a
* long ID degrades to "not filterable by chat" rather than failing to start.
*/
export function chatRunTags(chatId: string, userTags: string[] = []): string[] {
const chatTag = `chat:${chatId}`;
const tags = chatTag.length <= MAX_RUN_TAG_LENGTH ? [chatTag, ...userTags] : [...userTags];
return tags.slice(0, MAX_RUN_TAGS);
}
+6 -3
View File
@@ -79,6 +79,7 @@ import {
type InferChatUIMessageFromTools,
PENDING_MESSAGE_INJECTED_TYPE,
upsertIncomingMessage,
chatRunTags,
} from "./ai-shared.js";
import { auth } from "./auth.js";
import { locals } from "./locals.js";
@@ -11590,9 +11591,11 @@ function createChatStartSessionAction<TChat extends AnyTask = AnyTask>(
// Auto-tag every chat.agent run with `chat:{chatId}` so the dashboard /
// run-list filter by chat works without the customer having to wire it
// up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
// SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot.
const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5);
// IDs too long to fit within the tag length limit get no automatic tag.
const tags = chatRunTags(
params.chatId,
params.triggerConfig?.tags ?? options?.triggerConfig?.tags
);
const clientDataMetadata =
params.clientData !== undefined ? { metadata: params.clientData } : {};
+2 -2
View File
@@ -28,7 +28,7 @@ import {
TRIGGER_CONTROL_SUBTYPE,
} from "@trigger.dev/core/v3";
import type { ChatInputChunk, ChatTaskWirePayload } from "./ai-shared.js";
import { slimSubmitMessageForWire } from "./ai-shared.js";
import { chatRunTags, slimSubmitMessageForWire } from "./ai-shared.js";
import { sessions } from "./sessions.js";
// ─── Type inference ────────────────────────────────────────────────
@@ -671,7 +671,7 @@ export class AgentChat<TAgent = unknown> {
},
...(this.triggerConfigDefault?.machine ? { machine: this.triggerConfigDefault.machine } : {}),
...(this.triggerConfigDefault?.queue ? { queue: this.triggerConfigDefault.queue } : {}),
...(this.triggerConfigDefault?.tags ? { tags: this.triggerConfigDefault.tags } : {}),
tags: chatRunTags(this.chatId, this.triggerConfigDefault?.tags),
...(this.triggerConfigDefault?.maxAttempts !== undefined
? { maxAttempts: this.triggerConfigDefault.maxAttempts }
: {}),
+5 -4
View File
@@ -71,6 +71,7 @@ import {
} from "../imports/ai-runtime.js";
import type { FinishReason, ModelMessage, Tool, UIMessage, UIMessageChunk } from "ai";
import type { ChatInputChunk, ChatTaskWirePayload } from "./ai-shared.js";
import { chatRunTags } from "./ai-shared.js";
// `StreamTextResult` is defined locally rather than imported from `ai`: its
// generic arity diverged (v6 `StreamTextResult<TOOLS, OUTPUT>`, v7
@@ -198,7 +199,8 @@ export type HeadStartHandlerOptions<TTools extends Record<string, Tool>> = {
/**
* Run options for the auto-triggered `handover-prepare` session run —
* tags, queue, machine, etc. Mirrors `chat.createStartSessionAction`.
* The `chat:{chatId}` tag is prepended automatically.
* The `chat:{chatId}` tag is prepended automatically when it fits within
* the tag length limit (see `chatRunTags`).
*/
triggerConfig?: Partial<SessionTriggerConfig>;
/**
@@ -528,9 +530,8 @@ async function openHandoverSession(opts: {
// Merge the customer's trigger options. `handover-prepare` and `chatId` in
// `basePayload` are ours and can't be overridden; the `chat:{chatId}` tag is
// prepended (SessionTriggerConfig.tags caps at 5).
const userTags = opts.triggerConfig?.tags ?? [];
const tags = [`chat:${chatId}`, ...userTags].slice(0, 5);
// prepended when it fits within the tag length limit (see `chatRunTags`).
const tags = chatRunTags(chatId, opts.triggerConfig?.tags);
const triggerConfig: SessionTriggerConfig = {
basePayload: {
@@ -96,12 +96,12 @@ describe("chat.createStartSessionAction — runtime", () => {
expect(lastStartBody?.triggerConfig.basePayload).not.toHaveProperty("metadata");
});
it("prepends chat:{chatId} to triggerConfig.tags and caps at 5", async () => {
it("prepends chat:{chatId} to triggerConfig.tags and caps at 10", async () => {
installStartFixture();
const start = chat.createStartSessionAction("fake-chat", {
triggerConfig: {
tags: ["org:acme", "a", "b", "c", "d", "e"],
tags: ["org:acme", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
},
});
await start({ chatId: "chat-tags" });
@@ -112,9 +112,27 @@ describe("chat.createStartSessionAction — runtime", () => {
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
]);
});
it("omits the chat tag when the chat ID would exceed the tag length limit", async () => {
installStartFixture();
const start = chat.createStartSessionAction("fake-chat", {
triggerConfig: { tags: ["org:acme"] },
});
const longChatId = "c".repeat(200);
await start({ chatId: longChatId });
expect(lastStartBody?.triggerConfig.tags).toEqual(["org:acme"]);
expect(lastStartBody?.externalId).toBe(longChatId);
});
it("forwards maxDuration, region, and lockToVersion from triggerConfig", async () => {
installStartFixture();