Skip to content

fix(mcp): improve diagnostics, daemon resilience, and chart UX - #243

Open
StefanSteiner wants to merge 29 commits into
tableau:mainfrom
StefanSteiner:codex/hyperdb-mcp-agent-ux
Open

fix(mcp): improve diagnostics, daemon resilience, and chart UX#243
StefanSteiner wants to merge 29 commits into
tableau:mainfrom
StefanSteiner:codex/hyperdb-mcp-agent-ux

Conversation

@StefanSteiner

@StefanSteiner StefanSteiner commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR is a focused HyperDB MCP UX, diagnostics, and resilience release. It preserves the public Rust structs and the existing 33-tool surface while making installation/runtime state easier to diagnose, routed results more explicit, daemon handling safer, and charts more useful for operational analysis.

Diagnostics, installation, and daemon identity

  • Adds a side-effect-free hyperdb-mcp doctor command with human and JSON output. It reports installation identity, paths, persistent database state, daemon discovery/health, and actionable warnings without starting Hyper, creating state, or mutating the database.
  • Reports npm launcher/native executable identity safely, including non-UTF-8-safe path handling and bounded diagnostic rendering.
  • Enriches daemon discovery and STATUS records with additive build identity while preserving the public DaemonInfo shape and compatibility with legacy discovery files.
  • Makes CLI daemon status honor explicit ports in either supported argument position and scans only when no port was given.

Hyperd lifecycle and recovery

  • Routes daemon health control messages to the effective discovered health port instead of assuming the configured base port.
  • Releases the Engine mutex before health-plane I/O, so slow reports cannot serialize unrelated status or query work.
  • Adds bounded health I/O with absolute deadlines and response-size limits; hardens accepted TCP sockets on macOS/Darwin.
  • Detects connection-lost failures, reports them to the daemon, drops the stale Engine, and permits the next request to establish a fresh connection.
  • Keeps daemon restarts rate-limited and validates cleanup/reaping behavior in real child-process regressions.

Database routing and error semantics

  • Adds additive resolved_database metadata to query, execute, data, KV, and copy success results so clients can tell whether work used local, persistent, or an attached database.
  • Makes local/persistent/attached routing explicit in tool schemas and resource/status output.
  • Classifies SQLSTATE 55006 as RESOURCE_BUSY only at the reserved persistent attachment boundary; unrelated SQL failures keep their existing mapping and raw diagnostics.
  • Retains read-only behavior while accurately documenting the writable-attachment requirement for KV operations.

Status, resources, and catalog contract

  • Unifies full and degraded status identity data, including MCP/API versions, installation facts, default database, busy state, watcher state, attachment state, and read-only mode.
  • Ensures degraded status returns promptly even when Engine initialization is blocked.
  • Updates hyper://workspace compatibility resources to describe the separate local and persistent database model rather than a collapsed workspace mode.
  • Pins the generated catalog to the existing 33 public tools, excludes doctor from MCP tools, and measures the typed catalog payload against its 57,344-byte budget.

Chart improvements

  • Adds routed database metadata to chart responses and supports delivery/presentation controls such as inline vs. file output, overwrite behavior, labels, legends, color maps, and database selection.
  • Validates finite, strictly increasing effective ranges before rendering; rejects non-finite SQL values rather than silently converting or dropping them.
  • Preserves high-precision NUMERIC display labels, improves bar/legend layout, caps horizontal category ticks by available pixels, and keeps log meshes bounded across extreme domains.
  • Adds bounded child-process chart regressions so pathological rendering inputs cannot hang the suite indefinitely.

Documentation and compatibility

  • Aligns README, concise MCP README, smoke guide, demo, CLI help, generated schemas, resource text, and changelog with actual behavior.
  • Documents real HYPERD_PATH resolution, foreground daemon behavior, read-only guardrails, cross-database routing, chart behavior, Hyper export side effects, and doctor limitations.
  • Keeps public ChartOptions, render_chart, and DaemonInfo source-compatible; no version, manifest, or lock-file change is included.

Validation

Completed validation includes:

  • cargo fmt --all --check
  • strict workspace Clippy
  • HYPERD_PATH=/Users/ssteiner/dev/bin/hyperd cargo test -p hyperdb-mcp — 583 passed, 0 failed, 16 ignored
  • HYPERD_PATH=/Users/ssteiner/dev/bin/hyperd cargo test --workspace --exclude hyperdb-api-node --exclude hyperdb-bootstrap — 1,546 passed, 0 failed, 38 ignored
  • node --test hyperdb-mcp/npm/bin.test.js — 6 passed
  • node --check hyperdb-mcp/npm/bin.js
  • generated tool-schema contract — 33 tools; 52,825 / 57,344 bytes

Follow-up

#242 tracks an intentionally separate resilience improvement: recognizing and recovering a hyperd process that remains alive but is completely unresponsive (for example, after SIGSTOP).

Attribution

AI-assisted by ChatGPT 5.6 Sol and Terra.

A lock conflict on a caller-supplied .hyper file (SQLSTATE 55006, or a
legacy "already attached"/"file is locked" phrase from older hyperd)
previously reached the user via Engine::execute_command, whose generic
From<Error> conversion leaves 55006 as SqlError. Only the reserved
persistent-attach path reclassified it to RESOURCE_BUSY.

Add Engine::execute_attach_command plus the shared is_attach_lock_conflict
predicate and attach_lock_error mapper (mirroring persistent_attach_error),
and route AttachRegistry's ATTACH through it so a held file surfaces as
RESOURCE_BUSY with the effective path, preserved diagnostic/SQLSTATE, and
doctor-oriented recovery guidance. Unrelated 55006 errors keep their
generic mapping.
parse_hex_color stripped an optional '#', checked s.len() == 6, then
sliced s[0..2]/[2..4]/[4..6]. A six-byte string whose bytes are not all
ASCII (e.g. "1é234", where é is two UTF-8 bytes) has len() == 6 but its
byte offsets do not land on char boundaries, so the slice panicked and
aborted the chart request. Guard with !s.is_ascii() before slicing;
ASCII guarantees one byte per char, so every slice is a char boundary.

Adds regression coverage for the bare and '#'-prefixed six-byte
non-ASCII forms.
In ephemeral-only mode (no persistent database) doctor_client_log_path
falls back to resolve_log_dir(None), which keys the log directory to the
doctor process's own PID. A separately running MCP server logs under its
own per-process directory, so the reported path can never correspond to a
real session. Emit an 'ephemeral_client_log_path_illustrative' warning so
the report states this instead of presenting the path as authoritative.
The same-directory fallback branch of findBinary() reused
sourcePackagePath, which points at the platform *subdirectory*'s
package.json — a path that does not exist in this branch (the manifest
sits next to the binary in __dirname). launcher_info.platform.package_path
therefore reported a nonexistent file. Recompute it as
join(__dirname, 'package.json') so launcher diagnostics report the
manifest that is actually present.
… on slow CI

The non-blocking health listener slept 100ms between accept() attempts on
the WouldBlock path. The doctor network phase budgets only a few hundred ms
for a STATUS round-trip; on slow CI runners (macos-14 has ~3 CPUs) a 100ms
idle gap between accepts could push the accept past that window, failing
real_health_listener_accept_cadence_fits_doctor_budget. Drop the idle
sleep to 5ms — well within the doctor budget and the 650ms watchdog the
doctor tests assert — without meaningfully raising idle CPU.
…tions

std::fs::canonicalize prepends the \\?\ verbatim prefix on Windows, but
the paths the doctor binary reports come from current_dir/current_exe,
which are un-prefixed. The sandbox root and expected-executable assertions
compared a prefixed expected path against an un-prefixed reported one and
failed only on Windows CI. Add canonicalize_for_test (mirroring
ingest.rs's prefix stripping, leaving genuine UNC paths alone) and use it
for both. No-op on non-Windows.
cargo-audit fails on the yanked chacha20 0.10.0 pulled transitively via
rand's ChaCha20 RNG. 0.10.2 is a semver-compatible patch (lockfile-only,
no Cargo.toml or public-API change) that clears the yanked-crate advisory
and turns the audit job green.
The [0.5.0] section had two sibling '### Fixed' headings (a stray
single-bullet block above '### Added' plus the real one after
'### Performance'), which markdownlint flags as MD024/no-duplicate-heading.
Fold the stray NUMERIC-sign bullet into the canonical Fixed block, leaving
proper Keep-a-Changelog ordering (Added -> Changed -> Removed -> Performance
-> Fixed).

Add a root .markdownlint.json enabling MD024 siblings_only so legitimately
repeated section names (Added/Changed/Fixed) under different version
headings no longer trip the rule. Together these clear all four MD024
warnings the editor reported on CHANGELOG.md.
The macos-14 CI runner (~3 cores, CPU-saturated) intermittently failed
`real_health_listener_accept_cadence_fits_doctor_budget`: doctor reported
the daemon Missing instead of LiveFromScan.

Root cause is the per-socket-operation ceiling, not the health-listener
accept cadence. The earlier health.rs WouldBlock poll reduction (100ms->5ms)
was necessary hygiene but not sufficient here: the doctor's connection lands
in the listen backlog immediately, so its PONG/STATUS read — not the accept
poll — is the binding constraint. When a daemon is slow to start servicing
its accept loop, that read exceeded the old 150ms DOCTOR_NETWORK_PHASE_TIMEOUT
and doctor timed out.

Raise DOCTOR_NETWORK_PHASE_TIMEOUT 150ms->300ms (the actual fix, ~2x the
observed CI slack) and DOCTOR_DAEMON_TIMEOUT 400ms->500ms (the global
deadline), with rationale doc-comments. On success doctor returns as soon as
STATUS arrives, so the widened waits only cost latency when a daemon is truly
absent; 500ms stays comfortably under the 650ms watchdog the drip test
asserts.
…indows

`persistent_lock_keeps_mcp_available` asserted that the RESOURCE_BUSY
diagnostic names the exact effective persistent path via
`diagnostic.contains(effective_path)`. The diagnostic is the JSON-serialized
error text, so on Windows the path's backslashes are JSON-escaped (`\` ->
`\\`) and the raw un-escaped path never matches — a deterministic Windows-only
failure (Unix paths use `/`, which JSON does not escape, so it passed there).

Compare against the JSON-escaped form of the path instead. On Unix the path
has no backslashes, so the escaped form equals the raw path and the assertion
is unchanged; on Windows it now matches the escaped backslashes in the JSON.
Mirrors the existing `engine_tests.rs` sibling, which already dodges this by
asserting against the parsed `error.message` field rather than the JSON text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant