Skip to content

fix(eve): fail closed on mangled empty-delivery markers and unattended retry filler - #2386

Open
ace139 wants to merge 2 commits into
vercel:mainfrom
ace139:fix/empty-delivery-fail-closed
Open

fix(eve): fail closed on mangled empty-delivery markers and unattended retry filler#2386
ace139 wants to merge 2 commits into
vercel:mainfrom
ace139:fix/empty-delivery-fail-closed

Conversation

@ace139

@ace139 ace139 commented Aug 21, 2026

Copy link
Copy Markdown

Summary

A downstream production agent posted the literal string <evedev-empty-delivery/> into a team Slack channel: a model mistyped the empty-delivery marker, the exact-match sentinel check missed it, and the control token was delivered as an ordinary message. Investigating that incident surfaced a second, related failure in the empty-response retry path. This PR fixes both — the sentinel now fails closed where it previously failed open — and pins each fix with regression tests.

Bug 1 — a mangled marker is delivered as channel text

hasEmptyDeliverySentinel was two exact includes() checks. A model that means silence but corrupts the token fell through to the channel's message.completed default, and the token went out as the message.

Model's entire reply Before After
<eve-empty-delivery/> silenced silenced (unchanged)
<evedev-empty-delivery/> (the incident) posted to the channel silenced
<eve-empty-delivery></eve-empty-delivery> posted silenced
`<evedev-empty-delivery/>`. (fenced, trailing dot) posted silenced
"The tag <evedev-empty-delivery/> is corrupted." delivered delivered (unchanged)

The fix: a reply that is nothing but a sentinel-shaped tag now counts as the marker — a corrupted eve* name, closing or paired forms, casing, _ separators, inner whitespace, attributes, HTML-escaped brackets, wrapping backticks or a code fence, and trailing sentence punctuation. The exact sentinel keeps its existing match-anywhere behavior.

Bug 2 — unattended retries recover into conversational filler

When a scheduled app-auth run (no human present) replied empty, the recovery nudge led with "Answer now from the tool results above." Models complied: "I now have all the data I need." was delivered as the channel message — contradicting the conditional-delivery instruction injected as a system message in that same turn.

The fix: with conditional delivery enabled, the nudge leads with the marker option, offers answering second, and names filler as forbidden. It is unchanged when conditional delivery is off.

Also fixed while here: rawResponseMessages is now cleared only when the step's response contains a single assistant message. resolveAssistantStepText reads only the last assistant message, so a step that replied twice used to erase the earlier, genuinely delivered reply from durable history along with the marker.

Design decisions worth review attention

  • The tolerant arm is anchored to the whole message; the exact arm stays match-anywhere. Every caller suppresses the entire message on a match, so an anywhere-matching fuzzy pattern would let a reply that merely discusses the marker suppress itself. The existing tests pinning the exact arm's anywhere semantics are untouched.
  • The pattern is deliberately spelled for linear time. It reads raw model output, and the natural single-pattern spelling — \s* on both sides of an optional / — backtracks quadratically (~6s on a 120k-character tag). Two anchored passes (a [^<>]* tag shape and a lookahead-terminated body) remove the ambiguity structurally. Tests pin the linearity, not just the behavior.
  • One existing test expectation is inverted on purpose. <eve-empty-delivery> (missing slash) was previously asserted as not the sentinel; treating it as one is the point of this change, and the test carries a comment saying so.
  • The nudge remains a trailing user message. The in-code comment documents why: extraSystemNote is owned by chained tool-recovery for its own call shape, and a system prepend would invalidate the provider's cached prompt prefix.

Known limitations (called out in code rather than papered over)

  • A near-miss marker concatenated with real prose in a rare two-assistant-message step is still silenced on the history side but delivered on the emission side; closing that needs assistant-message boundaries the stream does not carry, since it exposes text-part ids and one message can span several.
  • Pre-existing and unchanged here: a terminal step with provider-executed tool calls can still carry the marker into stepOutput.

Validation

  • Reproduced both incident failures — the mangled marker delivered as channel text, and the unattended retry recovering into filler — and pinned each with focused regression tests, including the history regression where a marker step erased a sibling assistant reply.
  • The near-miss table covers 19 mangled spellings that must be silenced against 13 replies that mention or resemble the marker and must still be delivered.
  • 5 adversarial inputs assert linear-time rejection at 200k characters.
  • The full eve unit suite passes: 7,053 tests. The behavior that reached production is model output, so no automated test exercises a real provider; the runtime paths are covered through the harness.

Checklist

  • This change was requested or approved by a maintainer
  • I ran the relevant checks from CONTRIBUTING.md
  • I added tests and documentation where relevant
  • I added a changeset if this touches the published eve package
  • DCO sign-off passes for every commit (git commit --signoff)

The marker is an internal control token with no published documentation, so the changesets are the only user-facing prose this needs. Closes #2388, which reports both bugs with the observed production shapes.

Diff size

Docs — 2 files · +10 / -0

Two patch changesets, one per user-visible behavior change.

Implementation — 2 files · +137 / -13

The matcher accounts for most of it. Roughly half of empty-delivery.ts is comment explaining why the tolerant arm is anchored while the exact sentinel is not, and why the pattern is spelled to avoid catastrophic backtracking — both are non-obvious constraints that a future simplification would otherwise undo. The tool-loop change is small: the nudge text and the single-assistant-message condition on discarding a response.

Tests — 2 files · +169 / -3

Larger than the fix because the interesting property is a boundary, not a case: tables enumerate mangled spellings that must be silenced against near-misses that must still be delivered. The linearity assertions are separate, since a matcher this permissive is only safe if it cannot be made to backtrack.

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

@ace139 is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

ace139 added 2 commits August 21, 2026 22:59
The sentinel matcher was two exact includes() checks, so a model that
typed <evedev-empty-delivery/> fell through to the channel's
message.completed default and the literal control token was posted into a
team channel. Treat a whole message that is nothing but a sentinel-shaped
tag as the marker: a corrupted eve* tag name, any casing, `_` separators,
inner whitespace, an optional self-closing slash, a closing or paired tag
form, attributes, escaped angle brackets, wrapping backticks or a code
fence, and trailing sentence punctuation.

The tolerant pattern is anchored to the normalized message rather than
matched anywhere, because every caller acts on the entire message on a
match; a fuzzy anywhere-match would let a reply that merely discusses the
marker suppress itself. The exact sentinel keeps its anywhere semantics.

Matching happens in two anchored passes — a bracketed lone-tag shape, then
the tag body — so the only variable-length quantifier costs O(n) split
attempts of O(1) work. The obvious single-pattern spelling puts `\s*` on
both sides of an optional `/`, which backtracks quadratically: ~6s on a
120k-char tag, on a matcher that reads raw model output.

Signed-off-by: Soumyo <heysoumyo@gmail.com>
When conditional delivery is enabled the recovery nudge appended "Answer
now from the tool results above" and only then mentioned the marker. On
schedule app-auth runs there is no human to answer, and that framing
reliably produced conversational filler — channels received "I now have
all the data I need." as the delivered message, contradicting the
conditional-delivery contract the same turn injects as a system
instruction.

Lead with the marker option, name the filler shapes as forbidden, and
leave the non-conditional nudge untouched. The note stays a trailing user
message: extraSystemNote is how a chained recovery repeats its own call
shape, so claiming it here would clobber the unsupported-tool recovery's
note and invalidate the cached prompt prefix.

Also stop discarding a step's whole response on a marker match unless the
marker message is the entire response. resolveAssistantStepText reads only
the last assistant message, so a step that replied twice erased the
earlier, genuinely delivered reply from durable history along with the
marker.

Signed-off-by: Soumyo <heysoumyo@gmail.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.

Empty-delivery sentinel fails open: a mangled marker is posted to the channel as literal text

1 participant