Context
ask_knowledge_base currently returns an opaque Tool Error: synthesis_failed. Message: No message provided. for every query (verified live this session, 2× — including a 3-doc query). KB retrieval is healthy (neo-knowledge-base, 27,540 docs); the failure is the LLM synthesis step. Two stacked defects make the tool unusable.
The Problem
Defect 1 — synthesis times out at 30s. Live KB log: [SearchService] Synthesis failed after retrieval; returning degraded references: [OpenAiCompatible] ask_knowledge_base synthesis timed out after 30000ms (host=http://127.0.0.1:1234, model=gemma-4-31b-it). askSynthesisTimeoutMs is leaf(30000, …). A recent cluster already established that local gemma-4-31b latency exceeds prior budgets — #12805 raised the miniSummary timeout above measured local-model latency, #12722 aborts hung miniSummary provider calls, #12802 watchdog-bounds the backfill. The ask path has a larger prompt (up to 5 full documents) than a miniSummary yet keeps a 30s budget, so it times out on legitimate local synthesis. Independent of embedder contention — the timeout reproduces; it's budget-vs-latency, the same family as #12805.
Defect 2 — graceful degradation defeated at the MCP boundary. On timeout, SearchService.#createDegradedSynthesisResponse deliberately returns {answer, references, degraded:true, error:'synthesis_failed', reason} so the caller can "use the references directly while the synthesis provider recovers." But BaseServer.formatToolResult treats any object with an error key as a hard error (isError = 'error' in result), emits Tool Error: ${result.error}. Message: ${result.message || 'No message provided.'}, and discards answer, references, and reason (the degraded path sets reason, not message). The references it fetched are thrown away and the real cause is hidden — the degradation delivers nothing.
The Architectural Reality
ai/mcp/server/knowledge-base/config.template.mjs:150 — askSynthesisTimeoutMs: leaf(30000, 'NEO_KB_ASK_SYNTHESIS_TIMEOUT_MS', 'number').
ai/services/knowledge-base/SearchService.mjs:165 — #createDegradedSynthesisResponse returns the error:'synthesis_failed' + reason shape; :282 the timeout try/catch.
ai/mcp/server/BaseServer.mjs:370 — isError = 'error' in result; the error envelope reads result.message, never reason/answer/references.
- Precedent:
#12805 (miniSummary timeout right-size), #12722 (abort hung calls), #12759 (ask synthesis provider-routing via buildChatModel).
The Fix
- Right-size the timeout. Measure real
gemma-4-31b synthesis latency for a representative 5-doc ask prompt (clean embedder window), then raise askSynthesisTimeoutMs to a measured-safe budget and/or add an abort-guard mirroring #12805/#12722. Trimming context size is a secondary lever.
- Deliver the degraded response. Surface
references + a bounded reason on synthesis timeout/failure instead of an opaque error. Primary (low-blast): drop the top-level error key from the degraded response so BaseServer delivers it as content with degraded:true + reason (keep a machine-detectable degradedCode). Secondary (optional, broader): teach BaseServer.formatToolResult to attach structuredContent + surface reason even on error envelopes so future degraded-with-data responses aren't silently flattened.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
ask_knowledge_base result |
SearchService.mjs:165 #createDegradedSynthesisResponse |
On synth timeout/failure return {answer, references, degraded:true, reason, degradedCode} as content (no error envelope) |
references usable; degraded:true machine-detectable |
ai/mcp/server/knowledge-base/openapi.yaml |
live log timeout + opaque No message provided output |
askSynthesisTimeoutMs |
config.template.mjs:150 |
Raise to measured-safe budget (+ optional abort-guard) |
env NEO_KB_ASK_SYNTHESIS_TIMEOUT_MS |
leaf JSDoc |
2× live 30000ms timeouts on gemma-4-31b |
Decision Record impact
none (no ADR asserted). Embodies the same "no silent fallback that erases the failure signal" principle as #12450.
Acceptance Criteria
Out of Scope
- The KB chat-path contention/priority lane (
#12748) — this is timeout-budget + result-shape, not scheduling.
- Memory-core corrupted memories (
#12830).
- Switching the synthesis model/provider.
Avoided Traps
- "The ask tool is broken / the collection is corrupt." Falsified: KB healthy (27,540 docs); retrieval works; only synthesis times out.
- "It's embedder contention." The timeout reproduces; it's budget-vs-latency (the
#12805 family), independent of any concurrent embedding run.
- Fixing only the timeout. Even with a correct budget, synthesis can still legitimately fail/slow; the degraded-references delivery must work so
ask stays useful.
- Broad BaseServer rewrite as the primary fix. The surgical SearchService-side change (drop the
error key) is low-blast; the BaseServer structuredContent-on-error enhancement is a separate optional hardening.
Related
- #12740 — [Epic] Agent OS local-first AI provider defaults and cost-safety (parent)
- #12748 — interactive/batch priority lane for local chat-model requests (sibling; contention, not this)
- #12805 / #12722 / #12759 — local-model timeout right-size / abort-hung / ask provider-routing precedent
- #12450 — query_summaries empty (same "no silent fallback" principle)
Origin Session ID: 2feb15b9-1948-4553-9679-1419ed7eecf1
Handoff Retrieval Hints:
- Retrieval Hint: "ask_knowledge_base synthesis timeout 30000ms gemma degraded references swallowed BaseServer error envelope"
- Log signature:
[SearchService] Synthesis failed after retrieval; returning degraded references: [OpenAiCompatible] ask_knowledge_base synthesis timed out after 30000ms
- Code anchors: SearchService.mjs:165, BaseServer.mjs:370, config.template.mjs:150.
Authored by Claude Opus 4.8 as @neo-opus-grace.
Context
ask_knowledge_basecurrently returns an opaqueTool Error: synthesis_failed. Message: No message provided.for every query (verified live this session, 2× — including a 3-doc query). KB retrieval is healthy (neo-knowledge-base, 27,540 docs); the failure is the LLM synthesis step. Two stacked defects make the tool unusable.The Problem
Defect 1 — synthesis times out at 30s. Live KB log:
[SearchService] Synthesis failed after retrieval; returning degraded references: [OpenAiCompatible] ask_knowledge_base synthesis timed out after 30000ms (host=http://127.0.0.1:1234, model=gemma-4-31b-it).askSynthesisTimeoutMsisleaf(30000, …). A recent cluster already established that localgemma-4-31blatency exceeds prior budgets —#12805raised the miniSummary timeout above measured local-model latency,#12722aborts hung miniSummary provider calls,#12802watchdog-bounds the backfill. The ask path has a larger prompt (up to 5 full documents) than a miniSummary yet keeps a 30s budget, so it times out on legitimate local synthesis. Independent of embedder contention — the timeout reproduces; it's budget-vs-latency, the same family as#12805.Defect 2 — graceful degradation defeated at the MCP boundary. On timeout,
SearchService.#createDegradedSynthesisResponsedeliberately returns{answer, references, degraded:true, error:'synthesis_failed', reason}so the caller can "use the references directly while the synthesis provider recovers." ButBaseServer.formatToolResulttreats any object with anerrorkey as a hard error (isError = 'error' in result), emitsTool Error: ${result.error}. Message: ${result.message || 'No message provided.'}, and discardsanswer,references, andreason(the degraded path setsreason, notmessage). The references it fetched are thrown away and the real cause is hidden — the degradation delivers nothing.The Architectural Reality
ai/mcp/server/knowledge-base/config.template.mjs:150—askSynthesisTimeoutMs: leaf(30000, 'NEO_KB_ASK_SYNTHESIS_TIMEOUT_MS', 'number').ai/services/knowledge-base/SearchService.mjs:165—#createDegradedSynthesisResponsereturns theerror:'synthesis_failed'+reasonshape;:282the timeout try/catch.ai/mcp/server/BaseServer.mjs:370—isError = 'error' in result; the error envelope readsresult.message, neverreason/answer/references.#12805(miniSummary timeout right-size),#12722(abort hung calls),#12759(ask synthesis provider-routing viabuildChatModel).The Fix
gemma-4-31bsynthesis latency for a representative 5-doc ask prompt (clean embedder window), then raiseaskSynthesisTimeoutMsto a measured-safe budget and/or add an abort-guard mirroring#12805/#12722. Trimming context size is a secondary lever.references+ a boundedreasonon synthesis timeout/failure instead of an opaque error. Primary (low-blast): drop the top-levelerrorkey from the degraded response soBaseServerdelivers it as content withdegraded:true+reason(keep a machine-detectabledegradedCode). Secondary (optional, broader): teachBaseServer.formatToolResultto attachstructuredContent+ surfacereasoneven on error envelopes so future degraded-with-data responses aren't silently flattened.Contract Ledger Matrix
ask_knowledge_baseresultSearchService.mjs:165#createDegradedSynthesisResponse{answer, references, degraded:true, reason, degradedCode}as content (no error envelope)degraded:truemachine-detectableai/mcp/server/knowledge-base/openapi.yamlNo message providedoutputaskSynthesisTimeoutMsconfig.template.mjs:150NEO_KB_ASK_SYNTHESIS_TIMEOUT_MSDecision Record impact
none(no ADR asserted). Embodies the same "no silent fallback that erases the failure signal" principle as#12450.Acceptance Criteria
ask_knowledge_basereturns its rankedreferences+ a boundedreason(degraded marker), NOT an opaqueTool Error: synthesis_failed. Message: No message provided.askSynthesisTimeoutMsraised to a measured-safe budget for a 5-doc gemma-4-31b prompt (cite the measured latency); abort-guard if synthesis hangs.isErrorenvelope).Out of Scope
#12748) — this is timeout-budget + result-shape, not scheduling.#12830).Avoided Traps
#12805family), independent of any concurrent embedding run.askstays useful.errorkey) is low-blast; the BaseServerstructuredContent-on-error enhancement is a separate optional hardening.Related
Origin Session ID: 2feb15b9-1948-4553-9679-1419ed7eecf1
Handoff Retrieval Hints:
[SearchService] Synthesis failed after retrieval; returning degraded references: [OpenAiCompatible] ask_knowledge_base synthesis timed out after 30000msAuthored by Claude Opus 4.8 as @neo-opus-grace.