Skip to content

fix(mods): preserve approval order for passive turn context - #3773

Open
letta-integration[bot] wants to merge 3 commits into
mainfrom
fix/approval-mod-queue-order
Open

fix(mods): preserve approval order for passive turn context#3773
letta-integration[bot] wants to merge 3 commits into
mainfrom
fix/approval-mod-queue-order

Conversation

@letta-integration

@letta-integration letta-integration Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Context

A queued turn can contain an approval continuation followed by ordinary user or task-notification content. turn_start mods currently receive that finalized transport array, so a mod that prepends reminder content can move the approval away from index 0. Core then rejects the otherwise valid continuation with 409 PENDING_APPROVAL.

The failing order was:

goal/reminder context -> approval -> queued task notification

This PR adds the Letta Code primitive needed to produce:

approval -> passive mod context -> ordinary transformed/queued messages

Related Core behavior: letta-ai/letta#6392.

Root cause

Queue composition and mod input mutation happened at different ownership layers. The queue produced the approval-first control envelope, then turn_start exposed the raw Array<MessageCreate | ApprovalCreate> and accepted a replacement array with only shallow validation. That let content-oriented mods displace or replace approval objects after queue ordering had already been decided.

What changed

Shared turn composition

  • Adds a typed ModTurnQueueItem with kind: "context" for passive mod context.
  • Adds shared composition that always takes approval objects from the original control envelope.
  • Places passive mod context after approvals and before ordinary transformed messages.
  • Recognizes both current approval objects and the generated client's historical shape where the optional type discriminator is absent.

Mod event surface

  • Adds queueItems to turn_start events and results as the safe path for passive additions.
  • Validates returned/mutated queue items between handlers.
  • Restores the previous valid queue state when a handler throws or leaves invalid data, matching existing input rollback behavior.
  • Formally deprecates writable ModTurnStartEvent.input and ModTurnStartResult.input as compatibility-only. They remain temporarily for transforms of existing ordinary messages, but must not be used to inject, remove, or reorder messages.
  • Contains unsafe compatibility mutations at the control-envelope boundary while providing queueItems as the approval-safe injection path. Removing writable input still requires a narrower typed transform API for legitimate existing-message transforms.

Runtime consumers

  • TUI, listener, and headless turn-start paths consume passive context through the shared composer when turn_start already runs for a user-message turn.
  • TUI and listener preserve their existing user-message trigger contract; approval-only internal continuations do not invoke turn_start mods.
  • Passive queue items remain event-local data and do not independently schedule or wake a turn.

Changed-behavior matrix

Input/state Result
Approval + queued user/task notification + passive mod context turn_start runs; approval remains first and context precedes the notification
Approval-only internal continuation turn_start does not run; no passive context is injected
Compatibility mod prepends a user message through deprecated input Unsupported injection is contained; original approvals are restored first
Mod attempts to replace an approval object Replacement approval is discarded; original approval is preserved
Approval without optional type discriminator Preserved as control-envelope input
Queue-item handler throws or writes invalid queue state That handler's queue mutation is rolled back; later handlers continue
Passive context with no real user input Does not independently schedule or wake a turn
Ordinary user turn with no passive context Existing transformed message behavior is retained

Validation

Focused regression suite:

bun test src/mods/mod-engine.test.ts src/mods/mod-turn-queue.test.ts src/queue/turn-queue-runtime.test.ts

Result: 49 passed, 0 failed.

Full repository validation:

bun run check

Result: all 12 checks passed, including Biome and TypeScript.

Limits and follow-up

This PR is containment plus a safe injection path, not removal of the broad transform surface. Writable turn_start.input remains temporarily for compatibility with real transforms of existing ordinary messages, but it is formally deprecated and must not be used to inject, remove, or reorder messages. queueItems is the safe path for passive additions.

queueItems does not replace legitimate transforms such as image stripping or normalization. A narrower typed transform API that represents those operations without exposing the whole transport array is required before writable input can be removed.

This PR does not migrate the goal mod itself. A follow-up in letta-ai/mods should stop prepending raw turn_start.input and contribute its reminder through queueItems instead.

The shared composition, trigger policy, and mod-handler isolation paths have direct regressions. TUI, listener, and headless wiring is covered by the common typed path plus full repository validation; this PR does not add three separate end-to-end runtime harnesses. The deprecation contract is JSDoc-only, following the existing source convention; no mock-heavy runtime test was added for comments.

Risk and rollback

The main compatibility risk is ordering for mods that intentionally mutate approval objects through turn_start.input. That behavior is now deliberately blocked because approvals belong to the control envelope. Ordinary existing-message transformation remains temporarily supported through the deprecated compatibility field, and approval-only continuations retain the pre-PR behavior of not invoking TUI/listener turn_start handlers. Reverting these commits restores the previous raw-array behavior but also restores the approval-order failure.

Compose mod-provided context at the shared turn queue boundary so approval continuations remain first without allowing passive context to wake a turn by itself.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>
letta-integration Bot and others added 2 commits August 9, 2026 20:37
Overlord (agent-c2adbf5c-8419-4211-8cd8-3740db164974).
Restore the existing trigger contract so approval-only continuations do not invoke turn_start mods while mixed approval and queued-user turns still compose passive context safely.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>
Overlord (agent-c2adbf5c-8419-4211-8cd8-3740db164974).
Mark writable turn_start input as compatibility-only while directing passive additions through the approval-safe queue item path.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>
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.

0 participants