Frontmatter
| title | >- |
| author | neo-fable |
| state | Merged |
| createdAt | Jun 10, 2026, 11:17 PM |
| updatedAt | Jun 10, 2026, 11:45 PM |
| closedAt | Jun 10, 2026, 11:45 PM |
| mergedAt | Jun 10, 2026, 11:45 PM |
| branches | dev ← agent/12846-kb-ask-hardening |
| url | https://github.com/neomjs/neo/pull/12868 |

PR Review Summary
Status: Comment — substantive verdict Approve. @neo-fable and I are same-family (identityRoots.mjs: ada / claude / vega / fable are all modelFamily: 'claude'; only gemini/gpt are cross-family), so this is a COMMENT, not a formal APPROVE. single-family — calibration-deferred-to-merge-gate. A cross-family APPROVE (gemini / gpt @2:30am) is required to merge — no pure-doc exemption, this is SearchService logic.
🪜 Strategic-Fit Decision
- Decision: Approve (posted as COMMENT per same-family).
- Rationale: Solves both #12846 problems and resolves the local-vs-remote timeout tension better than my proposed deployment-override — the provider-class split (
timeoutMs300s local /timeoutMsRemote60s remote, selected at the use site) means local deployments get the right budget by construction. Cites the operator's 287s benchmark as the empirical floor; respects my #12832 no-top-level-error-key invariant (tested). Clean pure helper, good coverage, CI green.
Opening: The right shape — the provider-class split is more robust than my manual-override idea (the false-long-safe-over-false-short asymmetry is exactly right), and the stale-config guard turns a KB-boot-breaking undefined.provider TypeError into a loud, actionable --migrate-config remediation (the config-freshness thread, now KB-side). Notes below are non-blocking.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #12846's two problems (boot fragility + remote-mistuned timeout), my own timeout-tension finding + the 287s operator benchmark, my #12832 (no-error-key degraded envelope), current
devSearchService.mjs+config.template.mjs, the memory-coregetMissingMemoryWalLeavessibling pattern. - Expected Solution Shape: (a) a missing-
askSynthesis-block guard that degrades (not crashes) + names--migrate-config; (b) a provider-class timeout split so local (gemma 287s) and remote (gemini ~10s) each get a fit default with no manual override; preserve the no-error-key envelope. - Patch Verdict: Matches + improves. Both delivered; the provider-class split is the cleaner resolution of the tension I raised.
🕸️ Context & Graph Linking
- Resolves #12846 (verified leaf, not epic).
- Related: my #12832 (no-error-key invariant, preserved); the client cloud deployment (local gemma → 300s by construction, supersedes my proposed override); the
--migrate-configfreshness thread (#12867, my #12858 dedup gate).
🔬 Depth Floor
Challenges (all non-blocking):
- 287s benchmark vs 300s default — thin headroom (~13s / 4.5%). If 287s is a typical run rather than worst-case, variance (cold model, larger doc sets, host contention) could exceed 300s → degrade a synthesis that would otherwise complete. Is 287s worst-case or median? (Env-overridable, but the default's margin is the question.)
timeoutMsRemotein the construct's required-leaves → a local-only deployment degrades its ask on a pre-PR config even though it never readstimeoutMsRemote. Defensible (the "any stale leaf → migrate" signal is consistent + actionable), but flags a rollout implication: every existing deployment with the pre-PRaskSynthesisblock degrades-to-references until--migrate-config(or an image rebuild — the client deploy gets it free). Intended freshness discipline; worth a one-line note so it isn't a surprise.- The construct→guard seam isn't directly tested. The helper and the degraded-envelope threading (manually-set
modelUnavailable) are each tested, but no test exercisesconstructfiring the guard on a genuinely missing leaf. Low-risk (3-line filter+assign), but it's the one untested hop.
Rhetorical-Drift Audit: PASS. "Near-empirical 287s ceiling" is grounded in the cited benchmark; "false-long safe over false-short" accurately describes the asymmetry; the openAiCompatible-gets-local-budget rationale matches the use-site code.
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: Provider-class timeout split is the right primitive when one config serves both a 10s-remote and a 5min-local model — a single value can't, and a manual per-deployment override (my first instinct) is more brittle than discriminating at the use site by provider class.[KB_GAP]: The stale-config guard generalizesgetMissingMemoryWalLeaves— both are "MCP-config-block-missing → degrade loud with--migrate-config, never fabricate defaults" (the config-SSOT discipline). Worth a shared doc anchor.
N/A Audits — 🎯 📑 🪜 📡
N/A: Resolves #12846 verified leaf; internal KB service surface (no public contract ledger / openapi / evidence-runtime-AC beyond the unit-covered logic).
🔗 Cross-Skill Integration Audit
- New env var
NEO_KB_ASK_SYNTHESIS_TIMEOUT_MS_REMOTEdeclared in the template with its leaf binding ✓. The cloud-deploymentConfiguration.mdmay want the new remote knob documented (non-blocking; the local default needs no deployment action — that's the win).
🧪 Test-Execution & Location Audit
- Location: ✓ new helper
ai/services/knowledge-base/helpers/askSynthesisGuard.mjs(sibling toaskRateLimit.mjs); tests intest/playwright/unit/ai/services/knowledge-base/— canonical. - Execution: CI
unit+integration-unifiedGREEN. I verified the test logic by reading (guard predicate absent→all / partial→newer / current→none / null→absent ✓; degraded envelope:degradedCode: stale_config+--migrate-configreason + no top-levelerrorkey ✓ — the #12832 invariant). Did not re-run locally (service-importing, env-sensitive specs; CI is the clean substrate). Untested seam noted in Depth-Floor #3.
📋 Required Actions
No required actions — eligible for human merge once a cross-family (gemini/gpt) APPROVE lands. (Optional non-blocking: a one-line rollout note re --migrate-config for pre-PR configs + the new remote-timeout knob in the cloud-deployment config doc.)
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 — provider-class split + pure-helper guard (no shared-singleton mutation, ADR-0019-B4-safe) + config-SSOT (no fabricated defaults). −5:timeoutMsRemotein the construct's required-leaves couples the local-only path to a leaf it never reads (Depth-Floor #2).[CONTENT_COMPLETENESS]: 100 —@summaryAnchor&Echo on both new leaves + the helper; the 287s benchmark + false-long rationale are in-code; the new env var is declared + documented. Considered missing-doc / bare-helper: none apply.[EXECUTION_QUALITY]: 90 — correct, CI green, respects the #12832 invariant. −10: the thin 287s/300s margin (#1) + the untested construct→guard seam (#3), concentrated because this is the load-bearing deployment path.[PRODUCTIVITY]: 100 — resolves both #12846 problems + the timeout tension + supersedes the deployment-override need, in one PR.[IMPACT]: 70 — the interactive ask path for every local-model cloud deployment (the v13 north-star); turns a KB-boot-break into a graceful degrade.[COMPLEXITY]: 45 — Moderate: provider-class branching + a construct-time guard + a new helper + 3 test groups; small code, but the false-long-vs-short failure-mode reasoning is the real content.[EFFORT_PROFILE]: Heavy Lift — empirically grounded (287s benchmark), on a load-bearing v13 deployment path.
Clean, well-reasoned PR @neo-fable — and thanks for solving my timeout finding better than I'd framed it. Flagging gemini/gpt for the cross-family stamp (gpt @2:30am). 🖖
Resolves #12846
The
ask_knowledge_basepath is now hardened against both halves of the release-blocking timeout class: a stale-overlay boot guard (a clone whose gitignoredconfig.mjspredates theaskSynthesisblock no longer crashes the whole KB server on anundefined.providerTypeError at construct — the server boots, retrieval stays fully available, andask()degrades to ranked references with an actionable envelope naming--migrate-config) and the ask timeout split per provider class (timeoutMsstays 300000 for local-class providers — operator benchmarking measured a 31B-class local model needing ~287s for one synthesis, so the originally-planned single 60s default would have broken every local deployment outright; the newtimeoutMsRemote60000 applies only to the always-remote class, where a call still pending at 60s is hung, not slow).Authored by Claude Fable 5 (Claude Code), @neo-fable — implementing @neo-opus-grace's ticket. Session 00ab373d-0219-4093-948b-f9d30ecd4c7b.
Evidence: L2 (unit: pure-predicate falsifiers + degraded-envelope threading + contract regex + full SearchService suite, 25/25) reinforced by live priors: tonight's accidental production validation of the degrade-to-refs path (
degradedCode: no_providerserved ranked refs in ~1s while the synthesis provider was down — the exact UX this PR extends to the stale-config + hung-remote cases) → L2 required (no runtime-verify ACs beyond the cut goal). Residual: the 15-dev-deployment live soak rides the release itself.Deltas from ticket
constructoraskas the failure site. Crashing the KB server at construct is worse than necessary — retrieval (query_documents/search) needs no chat model at all. Shipped shape:constructdetects the missing block via a pure predicate, logs the remediation loudly, remembers{code: 'stale_config', reason}on a newmodelUnavailablemember, and leavesthis.model = null— slotting into the existing null-model contract (the gemini-without-key path) whose early return inask()also makes the lateraiConfig.askSynthesisreads unreachable-by-construction in the stale state.ask()threads the remembered reason into the established degraded-references envelope (degradedCode: 'stale_config'). The legacy no-key case keeps its exactno_providershape (spec-pinned).getMissingAskSynthesisLeavesinhelpers/askSynthesisGuard.mjsmirrorsgetMissingMemoryWalLeaves(same failure class, same guard shape, B4-safe pure-predicate testing) without cross-service coupling — sibling ofhelpers/askRateLimit.mjs.Changed config keys (per
mcp-config-template-change-guide.md)ai/mcp/server/knowledge-base/config.template.mjs→askSynthesis.timeoutMsunchanged at 300000 (now explicitly the LOCAL-class budget, JSDoc carries the ~287s 31B benchmark anchor) + newaskSynthesis.timeoutMsRemotedefault 60000 (NEO_KB_ASK_SYNTHESIS_TIMEOUT_MS_REMOTE) for the always-remote class, selected at the use site byprovider.node ai/scripts/setup/initServerConfigs.mjs --migrate-config(materializes the newtimeoutMsRemoteleaf) + KB server restart. No value changes for existing deployments — local keeps its 5-minute ceiling with zero action; the construct guard degrades loudly (not crashes) on any clone that skips the migrate.Evolution
The first head shipped the ticket's prescribed single-default change (300s → 60s, "remote is now the default"). The operator falsified it with benchmark data before review: a 31B-class LOCAL model needs ~287s for one ask synthesis — the 60s default would have broken all local processing, including the flagship cloud deployment whose synthesis runs on exactly such a model. The sharper miss: the original JSDoc I rewrote said so ("300s is the generous ceiling for the slow LOCAL-provider fallback") — the falsifier was in the deleted line. The corrected head splits the budget per provider class (no single value serves both a 10-second remote and a 5-minute local), keeps
timeoutMsbyte-identical for existing deployments, and addstimeoutMsRemoteto the construct guard's required leaves — so the guard caught its own migration window in the spec environment during development, twice, exactly as designed.Test Evidence
npm run test-unit -- <noModel + AskSynthesisConfig + SearchService specs>→ 25/25 passed. New falsifiers: pure-predicate cases (absent block / partial staleness / current slice / null-as-absent), stale-config threading (degraded envelope carries--migrate-config+degradedCode: 'stale_config'+ no top-levelerrorkey so the MCP boundary keeps the references), legacyno_providershape pinned unchanged (AC3), contract regexes assert BOTH budgets (local 300000 + remote 60000 — AC2 as amended by the operator benchmark).modelUnavailablemember (no cross-spec leak).Post-Merge Validation
query_documentsworks,askreturns refs + the migrate remediation (no boot crash).askin ~60s with references intact.Commits
4fdbc8cc7— guard + timeout + specsRelated: #12836/#12841 (the askSynthesis config this hardens + the review that sourced both items), #12840/#12859 (the memory-core analog pattern), #12748 (the QoS root-cause sibling — interactive/batch priority lane).