Skip to content

fix: preserve routed sub-agent task payloads by sanitizing before parse - #94

Closed
Wibias wants to merge 2 commits into
lidge-jun:mainfrom
Wibias:fix/routed-subagent-payloads
Closed

fix: preserve routed sub-agent task payloads by sanitizing before parse#94
Wibias wants to merge 2 commits into
lidge-jun:mainfrom
Wibias:fix/routed-subagent-payloads

Conversation

@Wibias

@Wibias Wibias commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

This fixes a routed multi-agent regression where spawned sub-agents woke up with the environment/context but without the actual task payload.

On the wire, the task body can arrive parked as plaintext inside an encrypted_content slot inside an internal agent_message. Two transformations are required before provider translation:

  1. rewrite the plaintext encrypted slot to input_text
  2. normalize the now-plaintext agent_message to a standard user message that parseRequest understands

Previously the sanitizer ran only on the native-bound path and only after parsing. Moving it before parseRequest exposed a second gap: the routed parser intentionally ignores unknown input item types, so merely making the content readable still left the entire agent_message undispatched. The sanitizer now performs both operations when the payload is entirely plaintext.

What changed

  • src/server/responses.ts

    • run sanitizeEncryptedContentInPlace on body.input immediately after expandPreviousResponseInput(body) and before parseRequest(body)
    • normalize a rewritten agent_message to { type: "message", role: "user" } only when no genuine encrypted content remains
    • preserve genuine backend ciphertext and mixed plaintext/Fernet payloads without coercing their container
    • remove the old native-only post-parse sanitize block
  • tests/multi-agent-compat.test.ts

    • mirror the real request order: sanitize/normalize raw input, then parse
    • assert both the container normalization and delivery of the complete NEW_TASK payload in parsed user messages

Root cause

parseRequest tolerates unknown input items at the schema boundary but only emits context messages for recognized item types. After the first version of this patch, the encrypted slot became readable input_text, yet its parent still had type: "agent_message". The parser therefore skipped it, producing one user message instead of two and dropping the spawned task.

This is why the regression test failed in CI with:

  • expected user-message count: 2
  • received user-message count: 1

The follow-up commit fixes that actual boundary rather than weakening the assertion.

Safety properties

  • Plaintext spawn payloads become ordinary user input before routed translation.
  • Genuine ciphertext remains byte-identical.
  • Mixed payloads retaining a Fernet token are not coerced into a user message.
  • The implementation remains confined to the request normalization layer.

Scope

This PR intentionally contains only:

  • src/server/responses.ts
  • tests/multi-agent-compat.test.ts

Verification

  • bun test tests/multi-agent-compat.test.ts --test-name-pattern "sanitize-then-parse delivers" - pass
  • bun test tests/multi-agent-compat.test.ts - 27 pass, 0 fail
  • bun run typecheck - pass
  • git diff --check - pass
  • full bun test - the spawn regression passes; the local run remains red on five unrelated pre-existing environment/integration tests (Cursor MCP live stdio x3, server pool-health, Windows service task)

The upstream cross-platform CI rerun was triggered by the follow-up push.

Wibias added 2 commits July 11, 2026 08:17
…ted sub-agents receive spawn payloads

sanitizeEncryptedContentInPlace now runs on the raw body BEFORE parseRequest, for every path. Previously it ran only for native-bound models (no provider prefix) and only after parsing, so routed models (anthropic/*, opencode-go/*) built their parsed messages from the unsanitized input: the agent_message branch dropped the encrypted_content part carrying the actual task text, and spawned sub-agents received an empty NEW_TASK envelope. _rawBody shares the same object reference, so the native passthrough keeps the rewritten parts too; genuine Fernet ciphertext stays byte-identical (looksLikeBackendCiphertext). Adds a regression test mirroring the exact handleResponses order (sanitize, then parse).
@Wibias
Wibias marked this pull request as draft July 11, 2026 06:31
@Wibias
Wibias marked this pull request as ready for review July 11, 2026 06:33
@lidge-jun

Copy link
Copy Markdown
Owner

Thanks! The unique delta (sanitizer normalization, 749d397) was cherry-picked onto dev as 6131577 on top of #93's commits (f29002d was patch-identical to #93's 64da27d, so it was skipped). Shipping in the next release.

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