The send-path repair branch set _request_result but not
_request_error_code, so the AGENT_REQUEST_FAILED lifecycle event fell back
to the misleading 'agent lifecycle failure'. Set THREAD_REPAIRED so logs
and middleware show the real outcome.
The repair paths (send-path _repair_pending_tool_calls and resume fallback)
called graph.update_state — the SYNC method — which internally invokes
AsyncPostgresSaver.get_tuple() and raises InvalidStateError ('Synchronous
calls to AsyncPostgresSaver are only allowed from a different thread').
The repair therefore always failed (surfacing as 'Event loop is closed' +
a dangling aget_tuple coroutine) and broken threads stayed broken.
Switch both repair sites to agent.aupdate_state (async checkpointer
interface) and align the mocked agent in tests. Verified live: aupdate_state
repaired the broken checkpoint of conversation 69651ca1 (pending calls → 0).
Two recurring failures from live logs (conversations 69651ca1 / a8c0dff8):
1. A checkpoint whose AI messages carry tool_calls without ToolMessages
(run crashed after the LLM emitted a call) makes every send raise
INVALID_CHAT_HISTORY with no recovery. The send path now repairs the
thread via _repair_pending_tool_calls: pending calls are answered with
synthetic error ToolMessages (THREAD_REPAIRED) so the user can retry.
2. Scenario tools scheduled from plain chat (no build_dashboard_test_scenario
UI intent) had no durable AgentRun, so the resume fallback refused with
SCENARIO_RUN_REQUIRED. _ensure_scenario_run now lazily creates the run
from the tool args (dashboard_context from scenario_json for
validate/resolve), mirroring the UIContextV2 scenario contract.
Observed in a live checkpoint: the pending scenario_validate tool call
carried a scenario_json that the LLM stream cut mid-document (ended
inside the unclosed outer object, len 3837). _parse_json_value failed
with a generic 'Could not extract JSON value' that neither the operator
nor the LLM could act on. Add _looks_truncated (unbalanced structure /
unterminated string at end) and report 'truncated/incomplete JSON' with
input length, so the model regenerates the full document.
The backend route binds the search query to `search`; a bare `q` param is
not bound, so search_dashboards and prefetch_dashboards silently returned
the unfiltered catalog (e.g. query 'Sales' listed all 11 dashboards).
Switch both to `search` and update the URL-contract test.
- search_dashboards: call /api/dashboards with page_context=other and
page_size=100 so the profile 'My Dashboards Only' filter can no longer
hide the whole catalog; parse available_total/effective_profile_filter
and report hidden-by-filter instead of a false 'no dashboards' answer
- prefetch_dashboards: same full-catalog context; fix dead code where
data=resp.json() sat after return '' inside the error branch, making
every 200 response raise NameError and the prefetch always return ''
- llm-status: ?force=1 bypasses the 30s health cache so the 'Retry now'
button performs a fresh probe instead of re-reading the stale status;
frontend keeps a single retry interval (previously stacked intervals
decayed the countdown faster than 1/s and fired duplicate probes)
- tests: agent tool/prefetch, backend route bypass + force param,
frontend retry/force coverage
Scenario compile returned 422 VALIDATION_ERROR because the LLM sent
human-readable case names (smoke, data_integrity, filter_propagation) as
selected_case_ids, but the compiler requires registered catalog ids
(B01-B09, C01-C07, T01-T03) and raised KeyError on unknown ones.
- tools_038._compile_objective: resolve case ids through _resolve_case_ids,
which (1) passes through registered ids case-insensitively, (2) maps
human-readable synonyms to closest catalog cases, (3) drops unresolvable
tokens — a free-form name can never reach the compiler as a KeyError.
- CompileScenarioInput.objective_json description now enumerates the valid
catalog id ranges and gives an example so the LLM stops inventing names.
- Tests: human-readable mapping, unknown-id dropping, dedupe/first-registered
order (test_tools_038_parse.py, 21 passed).
Verification: ruff clean, 21 tests pass.
- INV_3: capability_mapper.py — MapCaseImpl region closed with wrong ID
(MapCase); duplicate MapCase endregion removed; all region pairs now
match by EXACT ID (stack-verified)
- ATTN_3: tools_038 helpers (DualAuthHeaders/Post/GuardPermission) now
carry 'scenario' primary keyword in [SEMANTICS] for DSA grouping
- Full invariant audit: INV_1-8 + ATTN_1-4 verified (module <400,
CC<=10 via ruff C901=0, all non-root contracts <=150 lines)
- 108 tests green; index rebuilt
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes