What this is
A finding, latent for the same reason #7602 was: nothing registers the search slot today, so the wrong branch is unreachable on every host that exists. Surfaced while implementing #7602 (PR #7937), which fixed the sibling field on the same slot and left this one deliberately untouched — it is a separate decision on a separate field, and folding it into that PR would have widened a scoped card.
Observed
packages/runtime/src/http-dispatcher.ts, in getDiscoveryInfo()'s per-service status map:
search: searchRegistered ? svcAvailable(undefined,undefined,searchSvc) : svcUnavailable('search'),svcAvailable() returns handlerReady: true for any occupant that carries no __serviceInfo self-description. The map's own contract, stated a few lines above it, is:
handlerReady: true means the dispatcher has a real, bound handler for this route.
The dispatcher has no handler for search — no search entry in route-ledger.ts, no branch in http-dispatcher.ts, and the call site passes route: undefined precisely because there is nothing to advertise. So a filled slot yields a record that says, in one breath, "no route" and "the handler is ready".
That is the contradiction #4318 named and closed for cache / queue / job. Its remedy is already in this file: svcInProcess(name, svc), which returns handlerReady: false plus inProcessServiceMessage(name) — "this is a kernel-internal contract with no HTTP surface", said honestly. search is the one slot of that shape that was not converted, and the only reason the omission never showed is that no ISearchService has ever been registered (CORE_SERVICE_PROVIDER records 'search': null).
Why this is a finding and not a bug
Identical grading to #7602: declared and served agree today, by coincidence of an empty slot. The wrong record can only be produced by a host that registers a search service, and none exists in either repository.
Why it was left out of PR #7937
What a fix would look like
Almost certainly one line, mirroring #4318:
search: searchRegistered ? svcInProcess('search',searchSvc) : svcUnavailable('search'),Worth deciding rather than assuming, on one point: inProcessServiceMessage('search')'s wording was written for kernel-internal contracts (cache/queue/job). A search engine is not kernel-internal in the same sense — it is an external engine with no dispatcher surface — so the message may want its own phrasing even if the handlerReady: false half is settled.
A fix should also carry a test: the same shape PR #7937 used for the capability — register a search service, assert handlerReady === false — since like every other assertion on this slot, it is vacuous unless the fixture fills it.
Filed unassigned and without a domain:* label so routing stays the triage seat's call (packages/runtime ⇒ domain:cli per the domain table, if that is the disposition).
Related: #4318 (the same contradiction, fixed for cache/queue/job), #7602 / PR #7937 (the capability half of this slot), #2462 / ADR-0076 D12 (honest self-report).
What this is
A finding, latent for the same reason #7602 was: nothing registers the
searchslot today, so the wrong branch is unreachable on every host that exists. Surfaced while implementing #7602 (PR #7937), which fixed the sibling field on the same slot and left this one deliberately untouched — it is a separate decision on a separate field, and folding it into that PR would have widened a scoped card.Observed
packages/runtime/src/http-dispatcher.ts, ingetDiscoveryInfo()'s per-service status map:svcAvailable()returnshandlerReady: truefor any occupant that carries no__serviceInfoself-description. The map's own contract, stated a few lines above it, is:The dispatcher has no handler for search — no
searchentry inroute-ledger.ts, no branch inhttp-dispatcher.ts, and the call site passesroute: undefinedprecisely because there is nothing to advertise. So a filled slot yields a record that says, in one breath, "no route" and "the handler is ready".That is the contradiction #4318 named and closed for
cache/queue/job. Its remedy is already in this file:svcInProcess(name, svc), which returnshandlerReady: falseplusinProcessServiceMessage(name)— "this is a kernel-internal contract with no HTTP surface", said honestly.searchis the one slot of that shape that was not converted, and the only reason the omission never showed is that noISearchServicehas ever been registered (CORE_SERVICE_PROVIDERrecords'search': null).Why this is a finding and not a bug
Identical grading to #7602: declared and served agree today, by coincidence of an empty slot. The wrong record can only be produced by a host that registers a search service, and none exists in either repository.
Why it was left out of PR #7937
capabilities.searchis still presence-gated, so it over-promises the moment the slot is filled #7602's ruling was aboutcapabilities.search— whether the capability is advertised. This isservices.search.handlerReady— how the slot occupant is described. Different field, different producer contract (capabilitiesdescribes the host's HTTP surface;servicesdescribes what is registered), different remedy.capabilities.searchis still presence-gated, so it over-promises the moment the slot is filled #7602's declared file surface covered the capability block; PR fix(runtime): dispatchercapabilities.searchanswers a statedfalse, not bare slot presence (#7602) #7937 states this explicitly in its "out of scope" section rather than silently widening.What a fix would look like
Almost certainly one line, mirroring #4318:
Worth deciding rather than assuming, on one point:
inProcessServiceMessage('search')'s wording was written for kernel-internal contracts (cache/queue/job). A search engine is not kernel-internal in the same sense — it is an external engine with no dispatcher surface — so the message may want its own phrasing even if thehandlerReady: falsehalf is settled.A fix should also carry a test: the same shape PR #7937 used for the capability — register a search service, assert
handlerReady === false— since like every other assertion on this slot, it is vacuous unless the fixture fills it.Filed unassigned and without a
domain:*label so routing stays the triage seat's call (packages/runtime⇒domain:cliper the domain table, if that is the disposition).Related: #4318 (the same contradiction, fixed for
cache/queue/job), #7602 / PR #7937 (the capability half of this slot), #2462 / ADR-0076 D12 (honest self-report).