fix(search): a daemon error object is an ERROR with a code, never 0 hits (#526) - #545
Merged
Merged
Conversation
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (8)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Measured on production (#526, PR 4 of 4): a saturated daemon answered /search/hybrid with HTTP 200 and a JSON-RPC error body {"error": {"code": -32003, "message": "daemon busy: 8 MCP tool call(s) in flight …"}}. The REST transports returned it verbatim, the search helpers read .get("results") or [], and the CLI printed 0 hits, exit 1 — "the palace was reachable and had nothing to say". A busy daemon was indistinguishable from an empty corpus, and the depth banner would then have said no curated document was in the top N: #526's own error class. One classifier, _raise_if_daemon_error_object, keyed on the PAYLOAD (a dict with `error` and neither `results` nor `result`), called by all three transports (_call_daemon_rest, _post_daemon_rest, _call_daemon_tool). -32003 or "busy" raises DaemonBusyError; anything else a DaemonError with the "daemon error" prefix _fail_daemon keys on. cmd_search's except now goes through _fail_daemon, whose new busy branch emits a dict LITERAL — {"error", "code": "daemon_busy", "source", "detail", "route"} — exit 2, so the contract test's literal walker sees the key. `daemon_busy` joins the documented set in the header and in BRANCHABLE_CODES together. Where the failing call is an optimisation — the deeper fetch, auto-mode's hybrid fallback — the real hits are still returned and the daemon's words travel in `warnings`, which the header prints: "curated_first_rank: null" is read as "the depth was not checked", never as "nothing curated exists". Stacked on #536 (the header block it appends to). Part of #526 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
--next-seq said 163 after the final fetch, but #543 (open) already carries 163 on its branch; 164 avoids colliding with my own open PR (162 is on main via #536). commit: HEAD for the merge step to resolve. Four renderers (changelog, README, llms-full, python-api — DaemonBusyError and the classifier gained docstrings); check-docs clean. Part of #526 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tests.…` tests/ has no __init__.py, so `from tests.test_cli_daemon_error_contract import BRANCHABLE_CODES` is a namespace-package lookup that the editable .pth resolves to the MAIN checkout's copy — green on main and CI, red in every worktree (#546, proven there with a tests/__init__.py positive control). importlib.util.spec_from_file_location on the sibling next to __file__ reads the copy in THIS tree. Part of #526 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_raise_if_daemon_error_object raises for EVERY error envelope, so the
`raise DaemonError(f"daemon error {code}: {message}")` two lines below it
in _call_daemon_tool could never run (Oracle PART 51). Deleted. The
classifier's message keeps the route — "daemon error <code> on /mcp
<tool>: <message>" — which names the failing tool; the old message did
not. The "daemon error" prefix _fail_daemon keys on is unchanged.
Part of #526
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
a5d8903 164 landed with #541, 165 with #542 and 166 with #543; --next-seq after the final fetch says 167. The rebase also merged #543's `short=` and this PR's `warnings=` on _deep_fetch_when_nothing_curated and its two call sites — both kept. All four renderers; check-docs clean. Part of #526 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jphein
force-pushed
the
dream/feat-526-busy-error
branch
from
September 19, 2026 15:24
ba6a75d to
101d667
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(search): a daemon error object is an ERROR with a code, never 0 hits (#526, PR 4 of 4)
Consumer of #536's option-C error shape (
{error, code, source, status?, detail?}— lucid-error-contract owns the shape; this PR adds one key to the documented set and emits it). Part of #526 — deliberately not "Closes": #526 enumerates three fixes (banner → #542, retrieval → #534, dedup → #543), all merged before this PR; this fourth PR is a defect found while measuring those three (a busy daemon rendering as an empty corpus), adjacent to the umbrella rather than one of its parts. #526 can be closed by hand once this lands, citing the four.Defect, measured on production. A saturated daemon answered
/search/hybridwith HTTP 200 and{"error": {"code": -32003, "message": "daemon busy: 8 MCP tool call(s) in flight (PALACE_MCP_TOOL_MAX_INFLIGHT=8)"}}.The REST transports returned it verbatim, the search helpers read
.get("results") or [], and the CLI printed0 hits, exit 1. A busy daemon was indistinguishable from an empty corpus — and the depth banner would then
have said no curated document was in the top N. That is #526's own error class inside #526's fix.
The base defect is wider than search, and one grade worse in one verb. On base 8408188,
status --jsonagainst a 200 +
-32003error object exited 0, echoing the error object as if it were the payload — aSUCCESS exit carrying an error (Oracle PART 51). On this head every daemon-backed verb that reaches a transport —
7/7 driven by Oracle with a busy body — exits 2 with
code: "daemon_busy", because the classification lives in thethree transports rather than in any one verb.
Change.
_raise_if_daemon_error_object(body, route)errorand neitherresultsnorresult.-32003or "busy" in the message →DaemonBusyError; anything else →DaemonError("daemon error <code> on <route>: <msg>")(the prefix_fail_daemonkeys its reachable line on). A body withresultsbeside anerrorpasses through._call_daemon_rest,_post_daemon_rest,_call_daemon_tool— so REST and MCP agree. In_call_daemon_toolthe classifier raises for every error envelope, so the pre-existingraise DaemonError("daemon error <code>: <msg>")below it was dead and is deleted; the message now carries the route (… on /mcp <tool>: …), which the old one lacked — kept on purpose. Tests that build that string by hand as a side effect are unaffected.cmd_search'sexcept DaemonError→_fail_daemon(e, want_json, route=…, query=…)(the one search-shaped site #536 did not reach). New busy branch emits a dict literal:{"error": prose, "code": "daemon_busy", "source": "daemon", "detail": <daemon words>, "route": …}, exit 2; prose:palace daemon at <url> is busy — <words>; retry shortlyon stderrdaemon_busyappended to the header line AND toBRANCHABLE_CODESintest_cli_daemon_error_contract.py— both or neither_deep_fetch_when_nothing_curated(..., warnings=)appendsdeeper fetch unavailable: <daemon words>; auto-mode's swallowed hybrid fallback appendshybrid fallback unavailable: …. The header already printswarnings, socurated_first_rank: nullis read as "the depth was not checked"Why a literal.
test_every_code_we_emit_is_in_the_documented_setwalks dict literals only (emitted ⊆ documented).Adding
daemon_busyto the header without a literal emitter would be unverified vocabulary reading as a contract.test_daemon_busy_is_emitted_as_a_dict_literalis the converse for this one key — safe here because the emitteris known to be a literal; the general converse would false-positive on
_fail_daemon's computedcode.Pre-registration amendment, stated. I pre-registered "2nd response busy → exit 2, never
curated_first_rank: null".That contradicts the tested contract that the deeper fetch is an optimisation (
test_widen_tolerates_a_failed_second_calldegrades to the shallow hits). Replacement: degrade AND carry the daemon's words in
warnings; joint producer→consumertest (
test_busy_deeper_fetch_keeps_the_shallow_hits_and_warns+test_the_header_prints_that_warning).Positive control on the base (#536 @9441576b, unfixed). 15/15 substantive tests FAIL there (4 exit/code, 3 degrade,
3 documented/literal, 4 classifier, 1 MCP). A filler test written to justify an import was deleted rather than kept.
Blast radius.
cli.py:DaemonBusyError(new class),_raise_if_daemon_error_object(new),_call_daemon_tool,_call_daemon_rest,_post_daemon_rest,_fail_daemon(busy branch),cmd_searchexcept,_deep_fetch_when_nothing_curated(
warnings=kwarg),_daemon_search_fast(addswarningsonly when non-empty — return shape otherwise unchanged),_daemon_search_hybrid,_daemon_search_auto; header contract line;BRANCHABLE_CODES._window_daemon_getisdeliberately untouched — different producer, same payload rule would apply if it ever returned a 200 error object.
Verification. Stacked on #536 @9441576b: full suite 7596 passed / 82 skipped / 0 failed (rc from the same
invocation); 330/330 across 12 files including #536's own contract/4xx/one-message/exit-propagation/window-source/cypher/stats
suites;
ruff check+ruff format --checkclean; check-docs (see entry commit). Lands after #536; expect onegenerated-only rebase at go.
🤖 Generated with Claude Code