feat(coach): partial-malformed observability on sentinel parser

The W3-of-v1.4.22 sentinel parser silently dropped malformed rows and
fired `coach.keyvalues.parse_failed` only when the entire block was
unusable. Mixed-format drift (one row breaks, the other six parse)
landed without any signal. Surface it.

- `SentinelParseResult.malformedEntries[]` records each skipped row
  with a typed reason (`missing_colon` / `value_overflow` /
  `label_overflow` / `no_END_marker` / `byte_overflow` /
  `schema_invalid`). Block-level failures (truncation, missing close
  marker) land here as synthetic entries so the wide-event always
  carries a typed cause.
- Per-row label and value caps moved out of the Zod schema into the
  parser so the row-level reason can be `label_overflow` /
  `value_overflow` rather than the catch-all `schema_invalid`.
- Chat route splits the annotation: `coach.keyvalues.parse_partial`
  when at least one row parsed AND at least one row failed, the
  existing `coach.keyvalues.parse_failed` stays for full-block
  failures. Both annotations carry the per-line reason array.
- Backwards-compatible `parseKeyValueLine` wrapper preserved.

Tests +5: label_overflow, value_overflow, byte_overflow,
mixed-malformed entry assertions, and a tryParseKeyValueLine
direct-API surface check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-11 01:20:01 +02:00
1 parent 921cb7d42f
commit 58ae9bca42
3 files changed
+205 -24

No files matched your search

+15 -2
View File
@@ -300,11 +300,24 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
if (sentinel.malformed) {
// Graceful degrade: log so ops can spot a provider whose
// sentinel format has drifted, but pass the prose through
// unchanged.
// unchanged. v1.4.23 H1 — split the annotation:
// - parse_partial: at least one row parsed AND at least one
// row failed (mixed-format drift on a single reply)
// - parse_failed: the whole block was unusable
// Both annotations carry the per-line `reasons` array so an ops
// dashboard can attribute the failure cause without re-running
// the parser.
const reasons = sentinel.malformedEntries.map((entry) => entry.reason);
const annotationName =
sentinel.keyValues.length > 0 && sentinel.malformedEntries.length > 0
? "coach.keyvalues.parse_partial"
: "coach.keyvalues.parse_failed";
annotate({
action: { name: "coach.keyvalues.parse_failed" },
action: { name: annotationName },
meta: {
kept: sentinel.keyValues.length,
malformedCount: sentinel.malformedEntries.length,
reasons,
promptVersion: PROMPT_VERSION,
},
});