LearnNewsExamplesServices
Frontmatter
id12091
titleSemanticGraphExtractor friction telemetry — detect empty-response distinct from parse-failure + skip retry amplification
stateClosed
labels
enhancementaiarchitecturemodel-experience
assigneesneo-opus-ada
createdAtMay 27, 2026, 11:24 AM
updatedAtJun 7, 2026, 7:16 PM
githubUrlhttps://github.com/neomjs/neo/issues/12091
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 27, 2026, 11:55 PM

SemanticGraphExtractor friction telemetry — detect empty-response distinct from parse-failure + skip retry amplification

Closed v13.0.0/archive-v13-0-0-chunk-14 enhancementaiarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on May 27, 2026, 11:24 AM

Premise

Empirical finding from PR #12076 Phase 2 benchmark run (operator-side, 2026-05-27 07:45Z):

Bucket Prompt Tokens TTFT median TTLT median outChars median
small 5K 193ms 2821ms 326
medium 30K 163ms 163ms 0
large 100K 735ms 735ms 0
max 200K 1387ms 1387ms 0

The ttftMs === ttltMs signature for medium/large/max indicates the provider opens then immediately closes the stream with empty output — most plausibly silent context-overflow rejection (LM Studio loaded-model context-window setting < bucket prompt size on this rig). Source: /tmp/gemma4-bench-results.json.

Current ai/services/graph/SemanticGraphExtractor.mjs:131-172 handles empty-response via the JSON-parse-failure path:

  1. provider.generate(messages) returns result.content = ""
  2. Json.extract(result.content) returns null → payload = null
  3. Retry loop fires with assistant-echo + feedback "Your previous response failed internal schema validation..."
  4. After maxRetries, RAW LLM DUMP logged as warning

Two friction-telemetry gaps:

  1. Misclassification: empty-response and malformed-JSON are conflated into the generic parse-failure path. No friction symptom is emitted distinguishing silent context-overflow rejection from actual schema mismatch. The two remediations differ (chunk session vs schema-repair prompt) but the telemetry hides which one applies.

  2. Anti-pattern retry amplification: The retry loop appends result.content (empty) + user feedback (~400 chars), growing the prompt monotonically. If root cause is context overflow, retries make it strictly WORSE — each retry adds tokens to an already-overflowing context. The pattern cannot converge.

Even after the cap-raise to 262K (#12063 / PR #12064), this gap persists: the cap is the upstream pre-check threshold, not a guarantee the provider can produce output at any size the cap allows.

Substrate V-B-A correction (cycle-1 ticket revision)

Credit @neo-gpt for surfacing the existing-primitive check (MESSAGE:4d9a5884 at 09:27Z). V-B-A on ai/services/memory-core/helpers/ConsumerFrictionHelper.mjs:

Surface Line Existing state
VALID_SYMPTOMS 65-72 Already contains 'context-overflow'
DETERMINISTIC_SYMPTOMS 63 Already contains 'context-overflow' (surfaces immediately, no 3-emission threshold)
deriveSuggestionKind() 167-168 Already maps 'context-overflow''compress-payload'
categorizeInvocationError() 150-156 Routes thrown errors matching /context|overflow|too large|maximum|exceed/i to 'context-overflow'

The gap is not in the friction-telemetry primitive (which is well-shaped already) but in SemanticGraphExtractor's detection path: empty result.content with NO thrown error never enters categorizeInvocationError(), so the primitive's deterministic-surface guarantee never fires.

This corrects the original ticket prescription which proposed adding a NEW 'empty-response' symptom + new schema fields. The right shape is reuse the existing 'context-overflow' symptom + add detection-and-emit at the extractor boundary.

Prescription (revised)

Add empty-response detection in SemanticGraphExtractor.executeTriVectorExtraction (and parallel TopologyInferenceEngine.extractTopology path) BEFORE Json.extract invocation. Reuse the existing 'context-overflow' primitive:

result = guardrailed.result;

if (!result?.content || result.content.trim() === '') {
    // Silent context-overflow: provider returned empty body without throwing.
    // Reuse the existing 'context-overflow' deterministic-symptom primitive
    // — auto-surfaces via DETERMINISTIC_SYMPTOMS + compress-payload suggestion via deriveSuggestionKind.
    logger.warn(`[SemanticGraphExtractor] Attempt ${attempt}: Empty response from provider for session ${session.meta.sessionId}; classifying as context-overflow (silent: ttftMs===ttltMs signature, no thrown error).`);

    emitConsumerFriction({
        symptom      : 'context-overflow',
        consumer     : 'SemanticGraphExtractor',
        model        : consumerModel,
        assetRef     : session.meta.sessionId,
        serviceDomain: 'dream-pipeline',
        note         : `Silent empty-response from provider (no thrown error). Prompt chars: ${inputPayloadText.length}. Signature: empty result.content after streaming completion.`
    });

    return null;  // SKIP RETRY — adding tokens to overflowing context never converges
}

// Existing path
payload = Json.extract(result.content);

The note field carries the empty-response-specific diagnostic for downstream rendering (GoldenPathSynthesizer + A2A handoff) without requiring schema extension.

Path-specific exit semantics:

  • SemanticGraphExtractor.executeTriVectorExtraction is multi-attempt with a retry loop → exits via return null; to abort the loop
  • TopologyInferenceEngine.extractTopology is single-attempt and void (no retry loop, no return value used by caller) → exits via return; (void early-return)

Acceptance Criteria (revised)

  • AC1: SemanticGraphExtractor.executeTriVectorExtraction detects empty result.content (null OR .trim() === '') BEFORE Json.extract invocation
  • AC2: Empty-response emits emitConsumerFriction({symptom: 'context-overflow', ...}) via the existing primitive — no new symptom enum entry
  • AC3: The note field carries the empty-response diagnostic ("Silent empty-response from provider..." + promptChars)
  • AC4: SemanticGraphExtractor.executeTriVectorExtraction empty-response returns null from the extraction method, aborting the retry loop (no token-amplification)
  • AC5: Same detection added to TopologyInferenceEngine.extractTopology (separate file, line ~66). Note: extractTopology is single-attempt and void — empty-response exit is return; (void early-return), NOT return null. No retry-loop to abort there.
  • AC6: Unit test injects empty result.content from a stub provider + asserts (a) friction symptom is 'context-overflow' (b) friction note contains "Silent empty-response" (c) retry loop did NOT fire (d) outer call returns null (e) aggregated friction is surfaced: true (deterministic-symptoms auto-surface, no 3-emission threshold needed)
  • AC7: No mutation to ConsumerFrictionHelper.mjs — primitive already correct per V-B-A above

Contract Ledger (revised — narrower scope)

Surface Before After Consumer
SemanticGraphExtractor.executeTriVectorExtraction empty-result branch falls through to Json.extract(""") → null → retry loop early-detect + emit 'context-overflow' friction + return null ConsumerFrictionHelper aggregator (existing primitive)
TopologyInferenceEngine.extractTopology empty-result branch falls through to Json.extract("") → undefined return (single-attempt, void method — no retry loop) early-detect + emit 'context-overflow' friction + return; (void early-return) ConsumerFrictionHelper aggregator (existing primitive)
Friction telemetry visibility for silent-overflow scenarios NOT EMITTED (silent failure mode) EMITTED via existing deterministic 'context-overflow' symptom (surfaces immediately) GoldenPathSynthesizer, A2A friction rendering, operator-facing REM cycle logs

Zero changes to ConsumerFrictionHelper.mjs — leverages existing well-shaped primitive. Detection-at-boundary fix, not telemetry-substrate fix.

Empirical Anchor

Benchmark JSON: /tmp/gemma4-bench-results.json (2026-05-27 07:45Z)

  • Provider: openAiCompatible (LM Studio on localhost:1234)
  • Model: gemma-4-31b-it
  • Signature: ttftMs === ttltMs + outputChars === 0 + outputChunks === 0 for ≥30K prompt-token buckets

Related

  • Parent context: Epic #12065 (Sandman incident-recovery)
  • Sibling: #12075 (Sub 9 regression tests — this ticket adds Hypothesis-14 to the inventory)
  • Sibling: PR #12076 (benchmark harness that surfaced the finding)
  • Sibling: PR #12064 (cap-raise — orthogonal; cap was raised but model still empty-responds; this ticket adds the safety net)
  • Sibling: #11447 (Brain-Pillar Consumer-Friction Feedback Channel — friction symptom consumer)

Discipline anchor (cycle-1 substrate correction)

Original ticket prescription proposed extending ConsumerFrictionHelper schema (new 'empty-response' symptom + rootCause + retryAdvised fields). @neo-gpt's V-B-A on the helper revealed the existing 'context-overflow' primitive already satisfies the use case via DETERMINISTIC_SYMPTOMS (immediate surface) + deriveSuggestionKind (compress-payload suggestion). Reuse-existing-primitive is the substrate-correct shape per feedback_check_neo_primitives_before_authoring_utils (Neo ships the primitive; use it correctly). Cycle-1 ticket revision narrowed scope from "extend telemetry substrate + add detection" to "add detection that uses the existing well-shaped substrate." Net reduction: 7 ACs → 7 ACs (semantic narrowing, not count), Contract Ledger 3 surfaces → 3 surfaces (all detection-side, zero helper mutation).

tobiu referenced in commit 7452f6f - "fix(ai): detect silent empty-response as context-overflow in graph extractors (#12091) (#12113) on May 27, 2026, 11:55 PM
tobiu closed this issue on May 27, 2026, 11:55 PM