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:
provider.generate(messages) returns result.content = ""
Json.extract(result.content) returns null → payload = null
- Retry loop fires with assistant-echo + feedback
"Your previous response failed internal schema validation..."
- After
maxRetries, RAW LLM DUMP logged as warning
Two friction-telemetry gaps:
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.
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() === '') {
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;
}
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)
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).
Premise
Empirical finding from PR #12076 Phase 2 benchmark run (operator-side, 2026-05-27 07:45Z):
The
ttftMs === ttltMssignature 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-172handles empty-response via the JSON-parse-failure path:provider.generate(messages)returnsresult.content = ""Json.extract(result.content)returns null →payload = null"Your previous response failed internal schema validation..."maxRetries, RAW LLM DUMP logged as warningTwo friction-telemetry gaps:
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.
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:VALID_SYMPTOMS'context-overflow'DETERMINISTIC_SYMPTOMS'context-overflow'(surfaces immediately, no 3-emission threshold)deriveSuggestionKind()'context-overflow'→'compress-payload'categorizeInvocationError()/context|overflow|too large|maximum|exceed/ito'context-overflow'The gap is not in the friction-telemetry primitive (which is well-shaped already) but in SemanticGraphExtractor's detection path: empty
result.contentwith NO thrown error never enterscategorizeInvocationError(), 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 parallelTopologyInferenceEngine.extractTopologypath) BEFOREJson.extractinvocation. 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
notefield carries the empty-response-specific diagnostic for downstream rendering (GoldenPathSynthesizer + A2A handoff) without requiring schema extension.Path-specific exit semantics:
SemanticGraphExtractor.executeTriVectorExtractionis multi-attempt with a retry loop → exits viareturn null;to abort the loopTopologyInferenceEngine.extractTopologyis single-attempt and void (no retry loop, no return value used by caller) → exits viareturn;(void early-return)Acceptance Criteria (revised)
SemanticGraphExtractor.executeTriVectorExtractiondetects emptyresult.content(null OR.trim() === '') BEFOREJson.extractinvocationemitConsumerFriction({symptom: 'context-overflow', ...})via the existing primitive — no new symptom enum entrynotefield carries the empty-response diagnostic ("Silent empty-response from provider..." + promptChars)SemanticGraphExtractor.executeTriVectorExtractionempty-response returnsnullfrom the extraction method, aborting the retry loop (no token-amplification)TopologyInferenceEngine.extractTopology(separate file, line ~66). Note:extractTopologyis single-attempt and void — empty-response exit isreturn;(void early-return), NOTreturn null. No retry-loop to abort there.result.contentfrom a stub provider + asserts (a) friction symptom is'context-overflow'(b) frictionnotecontains "Silent empty-response" (c) retry loop did NOT fire (d) outer call returns null (e) aggregated friction issurfaced: true(deterministic-symptoms auto-surface, no 3-emission threshold needed)ConsumerFrictionHelper.mjs— primitive already correct per V-B-A aboveContract Ledger (revised — narrower scope)
SemanticGraphExtractor.executeTriVectorExtractionempty-result branchJson.extract(""")→ null → retry loop'context-overflow'friction + return nullTopologyInferenceEngine.extractTopologyempty-result branchJson.extract("")→ undefined return (single-attempt, void method — no retry loop)'context-overflow'friction +return;(void early-return)'context-overflow'symptom (surfaces immediately)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)openAiCompatible(LM Studio on localhost:1234)gemma-4-31b-itttftMs === ttltMs+outputChars === 0+outputChunks === 0for ≥30K prompt-token bucketsRelated
Discipline anchor (cycle-1 substrate correction)
Original ticket prescription proposed extending
ConsumerFrictionHelperschema (new'empty-response'symptom +rootCause+retryAdvisedfields). @neo-gpt's V-B-A on the helper revealed the existing'context-overflow'primitive already satisfies the use case viaDETERMINISTIC_SYMPTOMS(immediate surface) +deriveSuggestionKind(compress-payload suggestion). Reuse-existing-primitive is the substrate-correct shape perfeedback_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).