Skip to content

fix(chat): keep steering, action and injected messages in the conversation - #4816

Open
ericallam wants to merge 16 commits into
mainfrom
fix/chat-agent-accumulator
Open

fix(chat): keep steering, action and injected messages in the conversation#4816
ericallam wants to merge 16 commits into
mainfrom
fix/chat-agent-accumulator

Conversation

@ericallam

@ericallam ericallam commented Aug 28, 2026

Copy link
Copy Markdown
Member

Summary

A steering message sent while the agent was answering

onTurnComplete: async ({ newUIMessages }) => {
  await db.saveMessages(newUIMessages);
},

Before: the steer reached the model and the browser, but never uiMessages or newUIMessages, so it was never saved and it disappeared on reload. Now it is in both.

An undo, edit, or regenerate

onAction: async ({ action }) => {
  if (action.type === "undo") chat.history.slice(0, -2);
},

Before: the rollback lived only in the running worker. It held while that worker stayed warm, then the next continuation booted from a snapshot that still contained the undone messages. They came back, minutes later, with no error. Now the action writes the snapshot.

A reply streamed back from an action

onAction: async ({ action, messages }) => {
  if (action.type === "regenerate") {
    chat.history.slice(0, -1);
    return streamText({ model, messages });
  }
},

Before: it reached the browser and nowhere else, so the user read a new answer the model had no memory of and the next turn carried on from the answer just replaced.

Now the captured message goes into the accumulator (replacing a message with the same id, otherwise appended, which is what makes a regenerate update the answer in place), the model-message lane is rebuilt from it, and the snapshot is written. The next turn's messages has it, and so does a continuation. A stream that fails part-way is no longer stored as though it finished either: the partial is kept, the failure reported.

Both action fixes are for platform-managed persistence. With hydrateMessages the runtime deliberately does not write, because your store is the source of truth, so a rollback and a streamed replacement are still yours to save, and chat.pipeAndCapture hands you the same message the runtime would have captured. The actions page now covers both models; it previously said only that persistence was your responsibility.

Injected system context

chat.inject([{ role: "system", content: "The user just upgraded to Pro." }]);

Before, on AI SDK 7: every provider rejected it (AI_InvalidPromptError from standardizePrompt, thrown before any provider call). The turn ended in the app's error fallback and persisted an assistant message with no parts, so the agent looked like it had stopped answering. Now it is appended to the model's instructions, where it is also treated as trusted, which is the reason to inject context in the first place.

Instructions are delivered by the helper, so a system-role injection needs it:

run: async ({ messages, signal }) =>
  streamText({
    ...chat.toStreamTextOptions(), // without this, a system injection never arrives
    model,
    messages,
    abortSignal: signal,
  }),

The conversational lane has no such requirement. An injection also applies to the next turn only, rather than repeating on every turn after it, and within that turn it is consumed once rather than once per read, so a run() that builds options more than once sees the same instructions in every build.

Verification

Each of the four has a test that fails without it, and each was run end to end against a deployed agent twice, once with the fix present and once with only that fix reverted, so the tests are known to fail in its absence rather than merely to pass in its presence. A 46-scenario sweep of the surrounding chat surface came back clean.

One later fix, recording only the steering messages a drain actually claimed, has unit coverage only: reproducing it needs a second consumer taking a record while shouldInject() awaits, which the deployed harness cannot produce.

The steering fix closes the durability half of that problem. The model-context half is unchanged, still covered by the expected-fail test from #4795.

@changeset-bot

changeset-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 692c060

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 27 packages
Name Type
@trigger.dev/sdk Patch
@trigger.dev/python Patch
@internal/dashboard-agent Patch
@trigger.dev/build Patch
trigger.dev Patch
@trigger.dev/core Patch
@trigger.dev/react-hooks Patch
@trigger.dev/redis-worker Patch
@trigger.dev/rsc Patch
@trigger.dev/schema-to-json Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer Patch
@trigger.dev/rbac Patch
@trigger.dev/sso Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@internal/tracing Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/testcontainers Patch
@internal/cache Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The SDK routes system injections into model instructions and keeps other injected messages in the conversation. Steering messages enter accumulated UI history and onTurnComplete.newUIMessages. Action responses and history mutations persist in snapshots while retaining the output cursor. The changes add documentation, patch changesets, harness support, handover coverage, and regression tests.

Merge Risk: 🟡 Moderate · up to e3318

The PR improves persistence for steering, action, and injected messages, but regenerated answers can still be duplicated in linear stores unless applications replace the old message atomically, and failed action snapshots can allow stale history to return later. Merge should wait for the persistence guidance and failure handling to be fixed or explicitly accepted.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 8 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: preserving steering, action, and injected messages in the conversation. It is concise and specific.
Description check ✅ Passed The description is detailed, relevant, and covers the behavior changes, persistence models, testing, and documentation. It does not include the template checklist, issue-closing line, or dedicated Cha…
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 8 files. (1 skipped: 1 unsupported.)

Full details: Description check

Explanation

The description is detailed, relevant, and covers the behavior changes, persistence models, testing, and documentation. It does not include the template checklist, issue-closing line, or dedicated Changelog and Screenshots headings, but the required change and testing information is substantially complete.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chat-agent-accumulator

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ericallam ericallam changed the title fix(chat): keep steered, injected and action-produced messages in the conversation fix(chat): keep steering, action and injected messages in the conversation Aug 28, 2026
coderabbitai[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@692c060

trigger.dev

npm i https://pkg.pr.new/trigger.dev@692c060

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@692c060

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@692c060

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@692c060

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@692c060

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@692c060

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@692c060

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@692c060

commit: 692c060

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/trigger-sdk/src/v3/ai.ts (2)

4267-4276: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep the model accumulator synchronized with injected steering messages.

Lines 4269-4276 update only chatCurrentUIMessagesKey. They do not update accumulatedMessages or turnNewModelMessages. After this response, the next turn in the same worker uses accumulatedMessages, so it omits steering that influenced the prior response. onTurnComplete.messages also omits that message.

Return the claimed UI and model messages from drainSteeringQueue(). Update both accumulator representations in each caller. Add a regression test that sends a steering message, completes the turn, then verifies the next run() call receives that message.


4267-4276: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add required development crumbs.

Add approved crumb instrumentation for these new behavior paths. If no approved namespace fits, ask before adding one.

  • packages/trigger-sdk/src/v3/ai.ts#L4267-L4276: add crumbs for steering claim and accumulator updates.
  • packages/trigger-sdk/test/inject-system-instructions.test.ts#L179-L234: add crumbs for the one-turn instruction-drain scenario.

As per coding guidelines, “Add crumbs as you write code — not just when debugging.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f069755c-6da5-41bc-8e27-c3b1350588ff

📥 Commits

Reviewing files that changed from the base of the PR and between fd2caf2 and ca7f086.

📒 Files selected for processing (6)
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/test/action-snapshot.test.ts
  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/test/chatHandover.test.ts
  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/test/steering-accumulator.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/test/chatHandover.test.ts
  • packages/trigger-sdk/test/action-snapshot.test.ts
  • packages/trigger-sdk/test/steering-accumulator.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (43)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: sdk-compat / Node.js 22.23 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Build and publish previews
🧰 Additional context used
📓 Path-based instructions (10)
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
We use vitest exclusively. **Never mock anything** - use testcontainers instead.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` (deprecated path alias)

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Use vitest for all tests in the Trigger.dev repository

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • packages/trigger-sdk/test/inject-system-instructions.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
🔇 Additional comments (1)
packages/trigger-sdk/test/inject-system-instructions.test.ts (1)

11-27: Replace mock-based chat coverage with the approved test strategy.

This test uses simulateReadableStream and MockLanguageModelV3. Use the repository Testcontainers-backed fixture instead.

As per coding guidelines, “We use vitest exclusively. Never mock anything - use testcontainers instead.”

Also applies to: 179-188

Source: Coding guidelines

@ericallam
ericallam marked this pull request as ready for review August 28, 2026 14:30
devin-ai-integration[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/trigger-sdk/src/v3/ai.ts (2)

4267-4276: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist claimed steering messages in raw accumulator paths.

drainSteeringQueue() updates only chatCurrentUIMessagesKey and chatTurnNewUIMessagesKey. ChatMessageAccumulator.prepareStep() and ChatTurn.prepareStep() pass their own queue into this helper, but neither raw accumulator is connected to these locals.

After a successful injection, the current inference sees the steering message. The next raw turn and caller-managed persistence do not see it because uiMessages and modelMessages were not updated.

Return the claimed UI and model messages, or add a callback that updates each raw accumulator after a successful claim. Deduplicate by message ID.


8036-8087: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add crumb markers for this new state transition.

Add // @Crumbs markers, or wrap this action stream capture and error path in a `// `#region` `@crumbs block. This path changes persistence order and error propagation.

As per coding guidelines, “Add crumbs as you write code — not just when debugging.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d29e0d9-c508-4a05-bf16-072254964879

📥 Commits

Reviewing files that changed from the base of the PR and between ca7f086 and 02c2e6b.

📒 Files selected for processing (3)
  • docs/ai-chat/background-injection.mdx
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/test/action-stream-accumulator.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (42)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: typecheck / typecheck
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build and publish previews
🧰 Additional context used
📓 Path-based instructions (11)
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
We use vitest exclusively. **Never mock anything** - use testcontainers instead.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/ai-chat/background-injection.mdx
  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` (deprecated path alias)

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
Use vitest for all tests in the Trigger.dev repository

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format

📄 CodeRabbit inference engine (docs/CLAUDE.md)

Files:

  • docs/ai-chat/background-injection.mdx
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
🧠 Learnings (2)
📚 Learning: 2026-06-16T09:19:47.637Z
Learnt from: d-cs
Repo: triggerdotdev/trigger.dev PR: 3960
File: apps/webapp/test/prismaInfrastructureErrorCapture.test.ts:0-0
Timestamp: 2026-06-16T09:19:47.637Z
Learning: In this repo’s Vitest setup, `vitest.config.ts` uses `globals: true`, so identifiers like `vi`, `describe`, `it`, and `expect` are available as globals in Vitest test files. During code review, do not flag missing `vi`/`describe`/`it`/`expect` imports as a runtime error or correctness issue when they’re used in `*.test.ts/tsx` or `*.spec.ts/tsx` files. Explicit imports are still preferred for consistency, but they’re not required for runtime behavior.

Applied to files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
📚 Learning: 2026-08-16T18:36:58.179Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 4537
File: packages/trigger-sdk/test/normalizeKeyString.test.ts:1-2
Timestamp: 2026-08-16T18:36:58.179Z
Learning: For related SDK `chat.agent` tests in the Trigger.dev repository—including chat channels, handover, snapshot, and transport-event coverage—keep new test files under `packages/trigger-sdk/test/` rather than colocating them with the `packages/trigger-sdk/src/v3/` source files.

Applied to files:

  • packages/trigger-sdk/test/action-stream-accumulator.test.ts
🔇 Additional comments (2)
docs/ai-chat/background-injection.mdx (1)

211-233: LGTM!

packages/trigger-sdk/test/action-stream-accumulator.test.ts (1)

99-171: LGTM!

The seed from payload.headStartMessages had no coverage for agents that do not
register hydrateMessages, and it reads unreachable: it sits inside
if (!hydrateMessages && couldHavePriorState), and couldHavePriorState is false on
a head-start run. It does fire, and this pins that.

Records the shape a persisting app has to handle, which is the part that actually
bites: by onTurnStart the accumulator is already ['user','assistant'], because
the warm route's partial is spliced in before the hook, so the incoming user
message is not the last one.
drainSteeringQueue used the injected uiMessage for span attributes, the
injection-confirmation chunk, the injected-ids set and onInjected — never the
accumulator. So the message reached the model and the browser, appeared in
neither uiMessages nor newUIMessages, and an app persisting from onTurnComplete
never learned it existed. The user steers, the agent obeys, the user reloads, and
their instruction is gone from the transcript and from every later turn's
context.

The asymmetry is the tell: a message that finds no step boundary falls back to
becoming its own turn and is accumulated normally. Only the path that worked lost
data.

Appended at injection time rather than turn end, so the order matches what
happened: after the message that started the turn, before the response that
answers it. Deduplicated by id, since a boundary can drain more than once.

The injection path had no test coverage at all — shouldInject appeared only in
ai.ts — because the harness had no way to deliver a message mid-turn. Adds
harness.sendPendingMessage() for that, which is also what a customer needs to
test steering in their own suite.
The snapshot is written on the turn-complete path, and an action is not a turn —
the block literally ends 'if (!isAction)'. So a chat.history mutation from
onAction lived only in the running worker's memory. Undo worked while that worker
stayed warm, then the next continuation booted from a snapshot still holding the
undone exchange and the messages came back. onAction is exactly where the docs
tell you to call rollbackTo, so this is the documented path silently not
persisting.

Writes the snapshot right after the action's override is applied, awaited for the
same reason as the turn-complete write: the agent may suspend straight after, and
in-flight promises do not reliably survive that.

An action has no turn cursor, so the write reuses the last one rather than
writing undefined — that would drop the resume point and make the next boot
replay from further back to rebuild what it could have read.
…ation

Returning a StreamTextResult from onAction piped it to the browser and stopped
there. The accumulator never saw it, no snapshot recorded it, and actions fire no
onTurnComplete — so the user read a good answer that the model had no memory of,
and the next turn carried on from the answer regenerate had just replaced. The
disagreement between the screen and the conversation was invisible until that
next turn contradicted it.

The action branch now captures what it pipes, using the pipeChatAndCapture that
already existed for exactly this, and appends the message to the accumulator.
Persistence beyond the snapshot is still the app's job, since an action fires no
turn hook — pipeAndCapture hands back the same message for that.

Also folds the snapshot write added for rolled-back history into one helper used
by both action paths, so a regenerate that both rolls back and answers writes
once rather than twice, and the cursor-preservation rule lives in one place.

The two fixes needed each other: with the rollback persisted but the response
dropped, a regenerate left the snapshot empty rather than stale — still wrong,
just differently.
chat.inject with role 'system' put the message into the conversation, which ai@7
rejects for every provider: standardizePrompt throws before any provider is
called. The next turn died with an error chunk reading 'An error occurred.' and
persisted an assistant message with no parts, so from the app's side the agent
had simply stopped answering.

The error message names the fix — use the instructions option — and Instructions
is string | SystemModelMessage | Array<SystemModelMessage>, so an injected system
block has a correct home. It is appended after the base prompt, which keeps the
prompt's position for caching and reads as a later amendment.

This makes the documented examples right rather than rewriting them to a
workaround. It also answers whether trusted mid-conversation context is
supportable: it is, and only this way. A message injected as 'user' is untrusted
by construction, and a well-aligned model says so and re-derives the answer from
tools instead. The docs now state which lane to use for facts and which for
directives.

A new instruction block changes the cached prefix, so the first call carrying it
misses the prompt cache. Only turns that actually injected pay it.
… paths

Record only the messages a steering drain actually claimed. The loop used the
offered batch, so a record another consumer took while shouldInject() awaited
was written into the accumulator for a turn it was never part of.

Drain the injected instructions once applied, matching the conversational
lane. Left in place they were re-applied by every later toStreamTextOptions()
call in the run, growing the prompt and changing its cached prefix each turn.

Clean a stopped action's partial response before it is committed, and skip
committing at all once the run is cancelled.
…finished

pipeChatAndCapture returns a stream failure rather than throwing it, so a
mid-stream failure in a response returned from onAction was committed as a
complete answer, snapshotted, and followed by a normal turn-complete with no
error — the browser saw the stream stop and the next turn built on the
truncated text. The partial is still kept; the failure is now surfaced with it.

Document that the instructions lane is delivered by chat.toStreamTextOptions(),
and that an injection applies to the next inference call only.
@ericallam
ericallam force-pushed the fix/chat-agent-accumulator branch from 02c2e6b to 52772b5 Compare August 28, 2026 20:47
The actions page said only that persistence was your responsibility inside
onAction, which is now wrong for platform-managed agents (the runtime writes
the snapshot) and too vague for app-owned ones, where a rollback and a
streamed replacement both need storing and there is no onTurnComplete to do
it in.
coderabbitai[bot]

This comment was marked as resolved.

The example saved the regenerated message without removing the one it
replaced, so a linear store would keep both and the next hydration would
return the pair. The undo branch already deleted; the regenerate branch now
does too, with a note that a history mutation is invisible to your database.
Drops the banned trivializing words, replaces future tense and "there is"
throat-clearing, and removes a "two things" lead-in that sat above three
bullets. Merges the two bullets that stated the same prompt-cache fact, and
stops claiming the injected block is appended as an array when it is merged
into a single instruction.
Recast each one as a comma, colon, parentheses, or two sentences rather than
swapping in a hyphen. Also removes a stray "simply", a future tense, and a
"had just been replaced" the previous pass missed in the changesets.
…ction pages

Covers the prose these pages already had, not only the new sections: the
frontmatter descriptions, code comments, the message-role table cell, the
injection-point list, and the see-also link descriptions. Each recast as a
colon, comma, parentheses, or two sentences.
Both stay patch. The double write only bites code that worked around a lost
message, and the silent action completion was the bug it now reports, so
neither is new functionality or an API break. The version cannot carry either
signal, so the changelog entries name them instead.

Also documents sendPendingMessage in the testing harness table, which listed
every other send method.
Draining the lane on read handed the injection to whichever
chat.toStreamTextOptions() call ran first and dropped it from the rest. A
run() that builds options twice, a classifier pass and then the answer, sent
the instruction to nobody if it passed the second one to streamText, with no
error anywhere. Consumption is now keyed on the turn, so every build in the
turn carries the same instructions and the turn after it carries none. A
hand-rolled loop with no turn context still drains on read.
devin-ai-integration[bot]

This comment was marked as resolved.

Consuming the instructions lane marked the blocks read but left them in it, so
an injection made in that turn's onTurnComplete queued behind them and the next
turn's clear destroyed both. Turn 1 carried its instruction and every turn after
it silently carried none, which is worse than the per-read draining it replaced.

The consumed blocks now move to turn-scoped state, so a second options build in
the same turn still sees them while the lane holds only what is pending. Also
guards the stash lookup: outside a turn both sides of the turn comparison are
undefined, so the optional-chained check matched and dereferenced nothing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants