Release classification: boardless operational P0 — local/cloud model-burn prevention, not a release-board expansion.
Context
Live operator evidence on 2026-06-24: during an active REM sleep graph extraction dream lease, LM Studio's loaded-model counter for google/gemma-4-26b-a4b grew from Gen 131,951 tok to Gen 140,439 tok. This happened while the orchestrator reported other maintenance tasks deferring behind REM.
This is not the same failure shape as the closed input-overflow ticket #13918. The important falsifier is that the visible counter keeps increasing after the request starts. A prompt/context input budget is fixed at send time; a growing Gen counter indicates an active generation/output path. The existing input guardrail still matters, but this incident exposes the other half of the budget: generated Tri-Vector output is not bounded at the call site.
Live duplicate sweep: checked latest 20 open issues at 2026-06-24T18:32Z; no equivalent found. Targeted GitHub searches for SemanticGraphExtractor max_tokens output cap, OpenAiCompatible finish_reason length streaming, tri-vector num_predict max_tokens, and LM Studio generated tokens runaway returned no equivalent issue. A2A all-status latest-30 sweep at 2026-06-24T18:32Z found no competing lane claim for this scope. Memory-mining queries for REM context/output cap and LMS generated-token runaway returned no prior mapping.
The Problem
SemanticGraphExtractor already has input guardrails and retry-loop truncation handling, but the Tri-Vector generation call does not pass an output-token cap or timeout:
ai/services/graph/SemanticGraphExtractor.mjs:333-334 calls provider.generate(messages, {reasoning_effort, responseSchema, responseSchemaName}) with no max_tokens, no Ollama num_predict, and no timeoutMs.
ai/services/graph/SemanticGraphExtractor.mjs:351-353 asks getCompletionFinishReason() / isLengthTruncatedCompletion() to classify provider length truncation, but that only works if the provider wrapper preserves the finish reason.
ai/provider/OpenAiCompatible.mjs:155-169 implements generate() by consuming stream() and returning only {content, raw: {message: {content}}}. It discards OpenAI-compatible choices[0].finish_reason, so the extractor's length-truncation branch is unreachable for LM Studio/OpenAI-compatible streaming.
ai/provider/OpenAiCompatible.mjs:253-299 only creates a timeout controller when timeoutMs or an upstream signal is provided. The Tri-Vector caller provides neither.
ai/provider/Ollama.mjs:372-450 defaults chat generation timeout to one hour and supports native options through preparePayload(), but the Tri-Vector caller does not pass num_predict, so Ollama can hit the same generated-output burn shape in cloud.
Result: a schema-constrained REM extraction can spend massive time/tokens generating output even when the input prompt is inside the safe band. If the provider eventually truncates, LM Studio/OpenAI-compatible currently drops the finish_reason, so existing fail-closed handling cannot observe the reason.
The Architectural Reality
The owner surface is the graph extraction call path, not global model config or deployment docs:
SemanticGraphExtractor.executeTriVectorExtraction() owns Tri-Vector prompt construction, schema-constrained output, retry semantics, and ConsumerFriction emission.
OpenAiCompatibleProvider.generate()/stream() owns LM Studio/OpenAI-compatible response normalization.
OllamaProvider.generate() owns native Ollama response normalization and already carries raw finish metadata if the caller requests bounded native options.
AiConfig.localModels.chat.* is the ADR 0019 source for local chat-model budgets. New budget leaves must be read at the use site and passed to providers; do not re-derive env values, mutate config, or silently fall back.
The Fix
Add a bounded generated-output budget for REM Tri-Vector extraction and make both supported local providers expose enough finish metadata for the existing extractor checks to work.
Recommended narrow shape:
- Add/read an
AiConfig leaf for Tri-Vector generated-output cap, or a role-scoped graph extraction output cap if the existing config taxonomy has a better local home.
- Pass that cap from
SemanticGraphExtractor.executeTriVectorExtraction() to providers:
- OpenAI-compatible / LM Studio:
max_tokens or the compatible field accepted by the server.
- Ollama:
num_predict via the native options path.
- Pass a task timeout for Tri-Vector generation so an output runaway is bounded by time as well as tokens.
- Preserve OpenAI-compatible streaming finish metadata in
OpenAiCompatibleProvider.generate() so SemanticGraphExtractor.getCompletionFinishReason() can see length / max_tokens and emit context-overflow instead of treating the output as an ordinary malformed JSON repair candidate.
- Keep the existing input guardrail intact. This ticket does not change the bytes/token estimator or safe input band.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
SemanticGraphExtractor.executeTriVectorExtraction() provider call |
This incident; #13918 retry-loop hardening |
Tri-Vector generation passes a finite output cap and timeout on every local provider call. |
Missing/invalid config fails loud per ADR 0019; no hidden defaults at the call site. |
Method JSDoc / inline budget comment. |
Unit test asserts OpenAI-compatible receives max_tokens; Ollama receives num_predict; timeout is passed. |
OpenAiCompatibleProvider.generate()/stream() normalized result |
Existing getCompletionFinishReason() contract |
Streaming completions preserve finish reason in raw so extractor truncation handling is reachable. |
If a server omits finish reason, behavior remains current content-only result. |
Provider JSDoc. |
Unit/provider test with SSE finish_reason: "length" reaches extractor abort path. |
OllamaProvider.generate() Tri-Vector options |
Cloud parity requirement |
Caller-supplied output cap maps to native options.num_predict; raw done_reason remains visible. |
If Ollama returns timeout/error, existing ConsumerFriction timeout/error path handles it. |
Provider option comment if touched. |
Unit test for payload shape and extractor done_reason length classification. |
| ConsumerFriction / REM run state |
#13918 visibility contract |
Length-capped output is surfaced as deterministic context-overflow / bounded-output friction, not silent JSON repair. |
No retry append after a capped/truncated response. |
PR evidence. |
Regression fixture proves generated-output truncation does not append-grow prompt and does not run unbounded. |
Decision Record impact
Aligned with ADR 0019. Any new config must be an AiConfig leaf read at the use site. No ADR amendment expected.
Acceptance Criteria
Out of Scope
- Full chunk-map/reduce REM graph extraction (#12073 / Discussion #12439).
- Input token estimator calibration or over-band re-serve termination (#13918 already closed that incident class).
- Killing or interrupting any currently running local model request.
- LM Studio model ejection/downscaling policy.
Avoided Traps
- Do not treat a growing
Gen counter as proof that prompt input exceeded context. A prompt is fixed at send time; growing token count points at generated output or provider accounting.
- Do not fix only LM Studio. Cloud uses Ollama, so the output cap must cover both provider families.
- Do not add defensive
Number() / fallback conversions around AiConfig leaves. ADR 0019 leaves define type and should fail loud.
Related
Related: #13918
Related: #12073
Related: #12065
Handoff Retrieval Hints: REM Tri-Vector generated output cap max_tokens num_predict finish_reason length LM Studio Gen counter; SemanticGraphExtractor provider.generate responseSchema no max_tokens OpenAiCompatible streaming finish_reason dropped
Release classification: boardless operational P0 — local/cloud model-burn prevention, not a release-board expansion.
Context
Live operator evidence on 2026-06-24: during an active
REM sleep graph extractiondream lease, LM Studio's loaded-model counter forgoogle/gemma-4-26b-a4bgrew fromGen 131,951 toktoGen 140,439 tok. This happened while the orchestrator reported other maintenance tasks deferring behind REM.This is not the same failure shape as the closed input-overflow ticket #13918. The important falsifier is that the visible counter keeps increasing after the request starts. A prompt/context input budget is fixed at send time; a growing
Gencounter indicates an active generation/output path. The existing input guardrail still matters, but this incident exposes the other half of the budget: generated Tri-Vector output is not bounded at the call site.Live duplicate sweep: checked latest 20 open issues at 2026-06-24T18:32Z; no equivalent found. Targeted GitHub searches for
SemanticGraphExtractor max_tokens output cap,OpenAiCompatible finish_reason length streaming,tri-vector num_predict max_tokens, andLM Studio generated tokens runawayreturned no equivalent issue. A2A all-status latest-30 sweep at 2026-06-24T18:32Z found no competing lane claim for this scope. Memory-mining queries for REM context/output cap and LMS generated-token runaway returned no prior mapping.The Problem
SemanticGraphExtractoralready has input guardrails and retry-loop truncation handling, but the Tri-Vector generation call does not pass an output-token cap or timeout:ai/services/graph/SemanticGraphExtractor.mjs:333-334callsprovider.generate(messages, {reasoning_effort, responseSchema, responseSchemaName})with nomax_tokens, no Ollamanum_predict, and notimeoutMs.ai/services/graph/SemanticGraphExtractor.mjs:351-353asksgetCompletionFinishReason()/isLengthTruncatedCompletion()to classify provider length truncation, but that only works if the provider wrapper preserves the finish reason.ai/provider/OpenAiCompatible.mjs:155-169implementsgenerate()by consumingstream()and returning only{content, raw: {message: {content}}}. It discards OpenAI-compatiblechoices[0].finish_reason, so the extractor's length-truncation branch is unreachable for LM Studio/OpenAI-compatible streaming.ai/provider/OpenAiCompatible.mjs:253-299only creates a timeout controller whentimeoutMsor an upstream signal is provided. The Tri-Vector caller provides neither.ai/provider/Ollama.mjs:372-450defaults chat generation timeout to one hour and supports native options throughpreparePayload(), but the Tri-Vector caller does not passnum_predict, so Ollama can hit the same generated-output burn shape in cloud.Result: a schema-constrained REM extraction can spend massive time/tokens generating output even when the input prompt is inside the safe band. If the provider eventually truncates, LM Studio/OpenAI-compatible currently drops the
finish_reason, so existing fail-closed handling cannot observe the reason.The Architectural Reality
The owner surface is the graph extraction call path, not global model config or deployment docs:
SemanticGraphExtractor.executeTriVectorExtraction()owns Tri-Vector prompt construction, schema-constrained output, retry semantics, and ConsumerFriction emission.OpenAiCompatibleProvider.generate()/stream()owns LM Studio/OpenAI-compatible response normalization.OllamaProvider.generate()owns native Ollama response normalization and already carriesrawfinish metadata if the caller requests bounded native options.AiConfig.localModels.chat.*is the ADR 0019 source for local chat-model budgets. New budget leaves must be read at the use site and passed to providers; do not re-derive env values, mutate config, or silently fall back.The Fix
Add a bounded generated-output budget for REM Tri-Vector extraction and make both supported local providers expose enough finish metadata for the existing extractor checks to work.
Recommended narrow shape:
AiConfigleaf for Tri-Vector generated-output cap, or a role-scoped graph extraction output cap if the existing config taxonomy has a better local home.SemanticGraphExtractor.executeTriVectorExtraction()to providers:max_tokensor the compatible field accepted by the server.num_predictvia the native options path.OpenAiCompatibleProvider.generate()soSemanticGraphExtractor.getCompletionFinishReason()can seelength/max_tokensand emitcontext-overflowinstead of treating the output as an ordinary malformed JSON repair candidate.Contract Ledger Matrix
SemanticGraphExtractor.executeTriVectorExtraction()provider callmax_tokens; Ollama receivesnum_predict; timeout is passed.OpenAiCompatibleProvider.generate()/stream()normalized resultgetCompletionFinishReason()contractrawso extractor truncation handling is reachable.finish_reason: "length"reaches extractor abort path.OllamaProvider.generate()Tri-Vector optionsoptions.num_predict; rawdone_reasonremains visible.done_reasonlength classification.context-overflow/ bounded-output friction, not silent JSON repair.Decision Record impact
Aligned with ADR 0019. Any new config must be an
AiConfigleaf read at the use site. No ADR amendment expected.Acceptance Criteria
Out of Scope
Avoided Traps
Gencounter as proof that prompt input exceeded context. A prompt is fixed at send time; growing token count points at generated output or provider accounting.Number()/ fallback conversions aroundAiConfigleaves. ADR 0019 leaves define type and should fail loud.Related
Related: #13918 Related: #12073 Related: #12065
Handoff Retrieval Hints:
REM Tri-Vector generated output cap max_tokens num_predict finish_reason length LM Studio Gen counter;SemanticGraphExtractor provider.generate responseSchema no max_tokens OpenAiCompatible streaming finish_reason dropped