1955ba8cef
A staging consultation submitted a three-domain plan, calculated all three successfully in 62.9s, and returned nothing but the ensureFinalResponseText fallback. The step budget was barely touched, so this is not the exhaustionc8d9ec64fixed. toModelDomainPlanContext returns two different shapes: a single domain flattens the evidence packet to the top level, several domains return only success, domains and consultations. Every hard output rule in jyotishInstructions is written against those top-level paths — evidence_contract.answer_policy, hard_blockers, rectification.boundary, status. None of them resolve in the multi-domain shape, and under a policy that forbids stating anything the server evidence does not support, silence is what the instructions ask for. Merge the packets into one top-level contract shaped exactly like the single domain one. Merging may only restrict: status takes the worst of ready > degraded > blocked, hard_blockers and missing_route_layers take the union, permission booleans need every domain to agree while limitation booleans need only one, and a field the domains genuinely disagree on is reported as unresolved rather than decided. available_layers is the one permission-shaped union, because a layer really was computed for some domain and denying it would deny real evidence. The natal projection is the same chart for every domain, so it is hoisted to one copy when the domains agree and left per-domain when they do not. The domain cap was six, advertised as six, and could never be paid for. Domains run sequentially at ~21s each against a cumulative 110s abort signal, so six is ~126s and four leaves nothing to write the answer with. Concurrency is not available: the Python API is a single GIL-bound ThreadingHTTPServer whose async work already sits behind a two-worker bounded queue that answers 503 when full. Derive the cap from the clock instead of choosing it — 110s minus a 45s answer reserve, divided by 21s, is three — and let the model-facing schema carry that bound so an unpayable plan is unrepresentable. A caller that builds a plan without that schema is truncated rather than refused, the loop stops early when the measured pace says the next domain will not fit, and either way the dropped domains are disclosed through omitted_domains and the receipt while status degrades, so a partial answer cannot be presented as complete. run.failed carried a code and nothing else, so the step durations, step budget and workflow route recorded byc8d9ec64were unavailable exactly when a run needed explaining. Send the same allowlisted receipt run.completed sends, built through publicConsultationRuntimeSteps so the internal failure code and model loop diagnostics stay server-side, and never let building it replace the failure event with a silent close. An agentic run that fails before streamAgentResponse exists never reached the settle-and-log path either, so the request-level catch now goes through the same entry point. Refs BUG-256, BUG-257, BUG-258. Co-authored-by: Cursor <cursoragent@cursor.com>