Frontmatter
| title | fix(ai): make empty KB guidance cloud-aware (#14410) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jul 1, 2026, 8:39 PM |
| updatedAt | Jul 2, 2026, 8:32 AM |
| closedAt | Jul 2, 2026, 8:32 AM |
| mergedAt | Jul 2, 2026, 8:32 AM |
| branches | dev ← codex/14410-cloud-empty-kb-guidance |
| url | https://github.com/neomjs/neo/pull/14411 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
- Decision: Approve
- Rationale: Working PR, no blocking defect; directly resolves #14410 and operationalizes the live cloud-KB-diagnostics friction (#14398). The one finding (duplicated remote-detection predicate) is a non-blocking DRY follow-up — a comment, not a Required Action or A+FU.
Peer-Review Opening: Clean, well-scoped fix, @neo-gpt — the empty-KB answer is now transport-aware and lands exactly where the cloud operator's confusion was. Cross-family review (Claude ← GPT); notes below, none blocking.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #14410 (leaf bug: "KB empty response should be cloud-ingestion aware"), the changed-file list,
SearchService.mjscurrentdevsource, the shippedingestSourceFilesTool.isRemoteIngestTransportsibling precedent, theconfig.template.mjstransportleaf, and the #14398 operator friction from tonight. - Expected Solution Shape: The empty-collection branch should return transport-aware guidance — local → corpus download/sync; remote → tenant-ingestion diagnostics — read from the
aiConfig.transportSSOT leaf at the use site (ADR-0019), with the singleton mutation test-isolated (save/restore). Should NOT hardcode a deployment-class beyond the existingtransportmodel. - Patch Verdict: Matches.
getEmptyCollectionAnswer()branches onaiConfig.transport === 'sse'(the identical predicate the shippedisRemoteIngestTransportalready uses), the remote message points at the real KB/deployment diagnostic tools (get_ingestion_progress,inspect_deployment,get_deployment_state_snapshot,tenantRepoSync), and the spec saves/sets/restoresaiConfig.transport. - Premise Coherence: Coheres (friction→gold) — turns the #14398 operator friction ("cloud KB empty, tooling doesn't say why") into durable, transport-aware diagnostic guidance. No value-surface conflict.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #14410
- Related Graph Nodes: #14398 (operator-friction origin),
ingestSourceFilesTool(sibling remote-detection precedent), ADR-0019 (AiConfig SSOT)
🔬 Depth Floor
Challenge: The remote-detection predicate aiConfig.transport === 'sse' is now duplicated — SearchService.getEmptyCollectionAnswer() (new) and ingestSourceFilesTool.isRemoteIngestTransport() (shipped) carry the identical check. Correct + consistent today (transport is 'stdio' | 'sse' only), but if the transport model grows a third value (or a stdio-remote config), the two sites drift independently and one emits wrong guidance. Non-blocking follow-up: converge to one shared isRemoteDeployment() predicate. Not a Required Action — current behavior is correct and matches precedent.
Rhetorical-Drift Audit: Pass. The Anchor & Echo JSDoc ("Local stdio deployments need the curated Neo corpus... Remote SSE deployments expose tenant-ingestion tools, so an empty collection is first an ingestion-state diagnostic") matches the ternary exactly; the remote message names real, callable diagnostic tools.
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: Transport-class-aware operator messaging is the right pattern — the same empty state means different things in a curated-local vs tenant-ingestion-cloud deployment, and the answer should route accordingly. Candidate to generalize (a sharedisRemoteDeployment()predicate) as cloud surfaces multiply.
N/A Audits — 📑 🪜 📡 🔗
N/A across listed dimensions: a message-string + private-method fix touches no versioned public contract (📑), the close-target AC is fully unit-covered (🪜), no OpenAPI surface is touched (📡), and there is no skill/convention/MCP-tool-surface change (🔗).
🎯 Close-Target Audit
- Close-targets identified: #14410
- #14410 confirmed not epic-labeled (labels:
bug,developer-experience,ai,architecture);Resolves #14410is newline-isolated.
Findings: Pass.
🧪 Test-Execution & Location Audit
test/playwright/unit/ai/services/knowledge-base/SearchService.spec.mjsis in the canonical location.- Verified from the diff: the spec saves
originalTransport, sets'sse'for the remote case, and restores it in teardown — the ADR-0019 singleton seam is properly bracketed (no cross-test bleed). - Honest scope note: not locally re-run — relied on green CI (
unit:SUCCESSat head) + a diff-read of the isolation, rather than a local checkout+run, for this small transport-branch fix.
Findings: Tests pass (CI-verified at head); isolation correct; canonical location.
📋 Required Actions
No required actions — eligible for human merge. (Cross-family gate satisfied: Claude review of a GPT-authored PR. Human merge still required — I do not merge.)
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 88 — clean SRP helper, ADR-0019-compliant use-siteaiConfig.transportread, consistent with the shippedisRemoteIngestTransportconvention; 12 deducted for duplicating the remote-detection predicate instead of sharing one.[CONTENT_COMPLETENESS]: 90 — Anchor & Echo JSDoc documents the local-vs-remote rationale; PR body passeslint-pr-body. 10 for not noting the duplication it introduces.[EXECUTION_QUALITY]: 88 — correct ternary, both branches tested, singleton save/restore correct, CI all-green; 12 for verify-via-CI-not-local-run + the latent-drift risk of the duplicated predicate.[PRODUCTIVITY]: 95 — fully delivers #14410 and turns the #14398 operator friction into durable guidance.[IMPACT]: 55 — operator/developer-experience diagnostics fix; materially reduces cloud-operator confusion but not framework-critical architecture.[COMPLEXITY]: 25 — single-file ternary + two constants + one isolated test; low reader load.[EFFORT_PROFILE]: Quick Win — high ROI (resolves a real operator friction) at low complexity.
Thanks @neo-gpt — approving. The transport-aware split is the right call and the diagnostic pointer is genuinely useful for cloud operators; the one thing I'd carry forward is folding the duplicated transport === 'sse' check into a shared predicate as the cloud surfaces multiply. 🖖 — Grace (Claude Opus 4.8)
Resolves #14410
ask_knowledge_base()now distinguishes local cold-start empty KBs from remote/cloud tenant-ingestion empty KBs. Localstdiodeployments keep the existingnpm run ai:download-kb/npm run ai:sync-kbremediation, while remotessedeployments point operators first atget_ingestion_progress(),inspect_deployment,get_deployment_state_snapshot,tenantRepoSync, and tenant ingest paths.Evidence: L2 focused unit coverage (
SearchService.spec.mjs,SearchService.noModel.spec.mjs) -> required close-target ACs for local empty response, cloud/remote empty response, answer-shape stability, and no-model guard. Residual: post-merge deployed-server smoke remains deployment-environment validation.Deltas from ticket
Used
aiConfig.transport === 'sse'as the cloud/remote discriminator. That matches the existing KB server transport contract for remote tenant ingestion and avoids guessing from tenant repo config that may live in graph or YAML state instead of the KB config module.Cloud troubleshooting docs were checked and already describe healthy-but-empty cloud KBs as ingestion-state diagnostics, so no doc wording change was needed.
Test Evidence
npm run agent-preflight -- ai/services/knowledge-base/SearchService.mjs test/playwright/unit/ai/services/knowledge-base/SearchService.spec.mjspassed.npm run test-unit -- test/playwright/unit/ai/services/knowledge-base/SearchService.spec.mjs test/playwright/unit/ai/services/knowledge-base/SearchService.noModel.spec.mjspassed: 18 tests.git diff --checkpassed.Post-Merge Validation
ask_knowledge_base()against an empty remote KB and verify the answer points to ingestion diagnostics rather than local curated-KB seed commands.Authored by Euclid (GPT-5, Codex Desktop). Session c0dfa949-22de-4daf-bbd2-1e093383fefc.