Skip to content

fix(flaggers): stop classifier structured-output truncation at max tokens - #3910

Merged
geclos merged 5 commits into
developmentfrom
cursor/flagger-json-parse-failure-1d6f
Jul 13, 2026
Merged

fix(flaggers): stop classifier structured-output truncation at max tokens#3910
geclos merged 5 commits into
developmentfrom
cursor/flagger-json-parse-failure-1d6f

Conversation

@geclos

@geclos geclos commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the escalating Structured-output JSON parse failure (flagger) signal in the Flaggers project (signal, ID gyloub76vor6ytuq12gkj77c).

Flagger classify traces on Bedrock Haiku were repeatedly ending with finishReason: length, exactly 2048 output tokens, and truncated assistant JSON like {"matched":true,"messageIndex":"1" — missing required feedback and a closing brace. Those malformed classify traces were then picked up by the deterministic output-schema-validation flagger, generating the signal.

Example traces: d16f58bc63c9ae4de462b0adeb8484d8, ca2bacf072a8395a18de7cfa3b9be507, 00e2d7778d81b2a663f7c7dd2f11e982.

Root cause

The classifier structured-output messageIndex was an open-ended field (first a JSON number, later a ^\d+$ string). Neither form is length-bounded in the tool schema Bedrock/Anthropic receives, so at temperature: 0 the model could get stuck emitting digits for messageIndex and never close the object — it exhausted the output budget (finishReason: length) and produced truncated, unparseable JSON. Prior work (#3369, #3377) changed the field's type but not its open-ended nature, so the same truncation kept recurring.

Fix

Keep messageIndex in the classify result, but make it structurally impossible to run away:

  • Bound messageIndex to an enum. buildProviderFlaggerOutputSchema is rebuilt per classify call so messageIndex is a z.enum of the trace's real transcript indices as string literals (capped at 200 to stay within Bedrock's grammar-size limits; omitted when the trace has no messages). The field becomes a choice among a small finite set, which the constrained/tool decoder cannot run away on.
  • Parsing stays lenient. A model that ignores the enum and emits any numeric string is still parsed; out-of-range values are dropped in parseFlaggerOutput rather than failing the whole classification.
  • maxTokens lowered 2048 → 512 as defense-in-depth — the payload is a boolean plus one or two short sentences.
  • Output contract tightened to require feedback under 300 characters and to instruct the model to pick one of the offered indices.

Existing no-output / schema-mismatch handling still maps any residual failure to matched=false.

Testing

  • pnpm --filter @domain/flaggers test (240 pass, 2 skipped)
  • pnpm --filter @domain/flaggers typecheck
  • Added unit tests asserting the generation schema enum-bounds messageIndex to the trace's indices, omits it for empty traces, and is the schema passed to the classify call
  • Added regression coverage for length-truncated output missing feedback (trace d16f58bc pattern)
  • Updated the malformed-JSON regression harness to replay with the per-row enum schema and the new contract wording

Follow-up

Do not mute or resolve the signal until after deploy and a human verifies occurrence drop-off. A separate improvement worth considering: label each transcript line with its index in the classify prompt so the enum values map to something the model explicitly sees, improving anchor accuracy.

Open in Web Open in Cursor 

…kens

Bedrock Haiku flagger classify calls were hitting the 2048-token output
limit with truncated JSON (matched + messageIndex, no feedback), which
surfaced as the structured-output JSON parse failure signal.

Drop messageIndex from the provider schema so classify output is only
matched + feedback, and lower the classifier maxTokens default to 512.

Co-authored-by: Gerard <gerard@latitude.so>
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
design-system Ready Ready Preview, Comment Jul 13, 2026 9:25am

Request Review

Re-introduce messageIndex in the classifier structured output, but rebuild
the generation schema per classify call so messageIndex is a z.enum of the
trace's real transcript indices instead of an open-ended numeric string.

The runaway that truncated output at the token cap came from messageIndex
being length-unbounded under Bedrock/Anthropic object generation: at
temperature 0 the model kept emitting digits until finishReason=length,
leaving invalid JSON. Enumerating the valid indices makes the field a
choice among a small finite set, which the decoder cannot run away on,
while preserving the message anchor in the classify result. maxTokens
stays at 512 as defense in depth.

Co-authored-by: Gerard <gerard@latitude.so>
Co-authored-by: Gerard <gerard@latitude.so>
@geclos
geclos marked this pull request as ready for review July 8, 2026 08:53
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown

PR #3910 — Diff Explanation

An interactive HTML walkthrough of this PR has been published:

Artifact (Cloudflare temporary): https://artifact-pr-3910-diff-review.modern-albertonykus.workers.dev

Claim within 60 minutes (permanent): https://dash.cloudflare.com/claim-preview?claimToken=2gv3j_qMiSvOXEoswWAH25z2xLu5T-srlnxvkuTIn6Y

The artifact covers:

  1. Background — how the flagger classifier and structured generation work
  2. Intuition — the root cause (open-ended digit grammar → token-cap runaway) and how bounding messageIndex to a finite enum of the trace's real indices fixes it
  3. Code walkthrough — all 5 files, diff blocks, the two-schema design (strict generation / lenient parsing), and the new tests
  4. Quiz — 5 interactive multiple-choice questions to verify understanding

…-json-parse-failure-1d6f

* origin/development: (71 commits)
  Fix signup attribution across latitude.so and console boundary (#3941)
  chore(deps): bump astral-sh/setup-uv from 7 to 8.3.0 (#3994)
  chore: update bundled models.dev data (#3987)
  chore(deps): bump @tanstack/react-virtual (#4002)
  Lock monitor incident conditions after creation (#3947)
  chore(deps): bump @typescript/native-preview (#3989)
  chore(deps): bump taiki-e/install-action from 2.82.7 to 2.82.9 (#3991)
  chore(deps): bump typing-extensions in /packages/platform/op-gepa/python (#3992)
  chore(deps): bump opentelemetry-instrumentation-together (#3993)
  chore(deps): bump opentelemetry-instrumentation-langchain (#3995)
  chore(deps): bump @slack/web-api from 7.10.0 to 7.18.0 (#3996)
  chore(deps): bump docker/build-push-action from 7.2.0 to 7.3.0 (#3997)
  chore(deps): bump openinference-instrumentation-openai-agents (#3998)
  chore(deps): bump openinference-instrumentation-crewai (#3999)
  chore(deps): bump aws-actions/configure-aws-credentials (#4000)
  chore(deps): bump typing-extensions in /packages/telemetry/python (#4001)
  Fix OAuth token validation to enforce live organization membership (#3917)
  feat(wrapped): add skills breakdown to Claude Code Wrapped (report V3) (#3978)
  chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#3902)
  chore(deps): bump anthropics/claude-code-action from 1.0.160 to 1.0.166 (#3990)
  ...

# Conflicts:
#	packages/domain/flaggers/src/use-cases/run-flagger.test.ts
@geclos
geclos merged commit fb3419d into development Jul 13, 2026
19 checks passed
@geclos
geclos deleted the cursor/flagger-json-parse-failure-1d6f branch July 13, 2026 09:30
@github-project-automation github-project-automation Bot moved this from Inbox to Done in Latitude LLM Roadmap Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants