Skip to content

Releases: tpsdev-ai/flair

Release list

v0.16.0

Choose a tag to compare

@tps-flint tps-flint released this 29 Jun 14:11
07232f7

🧪 CI clean-VM gate — exercise the REALISTIC user env so the #538 embeddings showstopper can't silently regress (ops-cd37)

The #538 fix (above) addressed a fresh sudo npm install -g @tpsdev-ai/flair leaving semantic search dead (model targeted the root-owned package dir; Harper-as-user couldn't write it). The uncomfortable part: CI never caught it. The existing docker/Dockerfile.test from-scratch job runs as root (no perms mismatch) and sets FLAIR_MODELS_DIR=/opt/flair-models (a writable override), so its "clean install" is not the user's environment — root + a pre-solved model path made the bug structurally invisible. The tarball smoke test (test.yml) also installs as root and its write/search round-trip uses a keyword-matching marker, so it passes even with embeddings dead.

This adds a gate that reproduces what a real user actually has:

  • New docker/Dockerfile.clean-vm + docker/test-clean-vm.sh. Builds the HEAD tarball (npm pack, the exact published file set), installs it globally as root (npm install -g → root-owned /usr/lib/node_modules package dir), creates a non-root flairuser, and runs flair init + the daemon as that user with NO FLAIR_MODELS_DIR override — the real default model-path resolution (<ROOTPATH=~/.flair/data>/models, the #538 default). The embeddings model is pre-staged at that exact user-owned path (to avoid an ~80MB live download stalling the seed loop); if #538 regresses and the model resolves back to the package dir, that staged copy is in the wrong place → EACCES on download → DEGRADED.
  • The assertion is genuine semantic recall, not keyword match. The gate asserts flair init reports Semantic search operational (the #533 in-init check, which prints DEGRADED but does not exit non-zero), then runs flair doctor as the hard gate — doctor performs the same embed→paraphrase round-trip (verifySemanticSearch: query "a cat hunting a mouse in the evening" vs. content "feline predator stalked its rodent quarry at dusk", zero keyword overlap, real semantic score > 0.05) and process.exit(1) on degraded. Keyword-only fallback cannot satisfy it. Embeddings dead → the gate FAILS.
  • Wired into .github/workflows/docker-test.yml as a new clean-vm-gate job that runs on PRs, alongside (not replacing) the existing from-scratch job — the from-scratch coverage stays, the gate adds the realistic non-root / no-override variant. Each Docker build uses a distinct GHA cache scope. Validated locally: builds + runs green on current main (post-#538) with a real semantic score; the assertion is semantic, so it would catch a regression that the old root-+-override CI could not.

🩺 Fix dead semantic search on a sudo/root-owned global install — model dir defaults to ~/.flair (ops-am0v)

A fresh sudo npm install -g @tpsdev-ai/flair left semantic search dead: the package landed root-owned (e.g. /usr/lib/node_modules), Harper runs as the user, so the embeddings model download hit EACCES and recall silently fell back to keyword-only. The flair doctor / flair init round-trip check (#533) caught it loud, but recall was still broken — this fixes the underlying cause.

Root cause (corrects the ops-9czl note). The blocker was the model path, not the node_modules/harper symlink. Flair loads itself as a Harper component in-place (harper run ., cwd = the package dir), and Flair's own embeddings wrapper (resources/embeddings-provider.ts) hard-coded the model dir to join(process.cwd(), "models") — i.e. inside the package dir. On a root-owned install that's read-only to the user-run Harper, so the model can't download and init() fails. Verified end-to-end with a faithful repro (read-only <packageDir>/models, isolated HOME/data/free port): pre-fix → ✗ Semantic search DEGRADED; the componentLoader's node_modules/harper symlink EACCES is caught-and-logged (componentLoader.js, no rethrow) and Flair imports nothing from harperdb, so it is non-fatal — the model path was the only real sink.

  • The embeddings model dir now defaults to a user-writable location, never the package dir (resources/embeddings-provider.ts). New resolveModelsDir() resolves, in order: FLAIR_MODELS_DIR (explicit override) → <ROOTPATH>/models (Harper's data dir — Flair passes ROOTPATH = ~/.flair/data when it spawns Harper, so this is user-owned and writable even under a root-owned install) → <cwd>/models only if a model is already cached there (backward compat for existing writable installs — reuse, don't re-download) → ~/.flair/data/models (last resort). Aligns with the principle that everything Flair writes lives under ~/.flair and the package dir stays read-only. FLAIR_MODELS_DIR (already used by docker/Dockerfile.test) is now an actually-wired override on the production path, not just a dev/docker affordance. Under the read-only-install repro, embed→paraphrase-search now round-trips with a real semantic score (~0.74) and doctor's #533 check passes.
  • Test harness reuses the pre-downloaded model via the override (test/helpers/harper-lifecycle.ts). With the new <ROOTPATH>/models default, the integration harness (fresh temp installDir per startHarper) would otherwise re-download the ~80MB model every run (HuggingFace 429-prone, #463/#465). The harness now sets FLAIR_MODELS_DIR to the repo-root models/ that CI/local pre-download into; a pre-existing parent FLAIR_MODELS_DIR still wins.
  • New unit coverage (test/unit/embeddings-models-dir.test.ts) asserts the resolution order, including the load-bearing invariant: a fresh install with no ROOTPATH and no cached model resolves to ~/.flair, never the read-only package dir. Full unit suite (1155) green; HNSW / agent-journey / smoke / durability integration tests green (real-embeddings paths exercised).

🛟 Loud Node-version preflight for flair-mcp — silent failure on old Node (ops-fomi)

The flair-mcp bin (dist/index.js) is an ES module: top-level imports are hoisted and the whole module graph is linked + evaluated before the file body runs. flair-mcp's deps (@modelcontextprotocol/sdk, @tpsdev-ai/flair-client and its transitive deps) need a modern engine, so on an old Node the import graph crashes during linking — before any in-file version guard could run. Result: a user wiring npx -y @tpsdev-ai/flair-mcp on an unsupported Node gets zero output and a dead MCP server, with no actionable signal. This is the same exposure flair's CLI had, fixed in #524 — now mirrored for the MCP server.

  • The flair-mcp bin now points at a CommonJS preflight shim (dist/mcp-shim.cjs, compiled from src/mcp-shim.cts). CJS evaluates top-to-bottom with lazy import(), so the Node-version check runs and prints before anything loads the ESM server or any modern dep. On an unsupported Node → an actionable message (flair-mcp requires Node.js >= 22. You are running Node.js X. ... https://nodejs.org/) + process.exit(1). On a supported Node → a transparent no-op that dynamically imports the server and hands off to runMcp().
  • The shim uses only ancient-safe syntax (var, plain functions, string ops, console.error, process.exit) so the guard itself can never fail to parse on the oldest Node a user could have. node --check confirms parse-safety.
  • src/index.ts now exports runMcp() — all runtime side effects (the FLAIR_AGENT_ID check, FlairClient construction, the parent-exit watcher, tool registration, the stdio connect) moved inside it, so merely importing the module (from the shim before the version check, or from a test) does nothing until runMcp() is called. Direct invocation (node dist/index.js, bun src/index.ts) still works via an import.meta.main entry-point guard.
  • engines.node bumped >=18>=22 to match flair's CLI and the deps' real floor, so npm install also warns on an unsupported Node. Postinstall now chmod +x the shim.
  • New unit test (test/mcp-node-preflight.test.ts) proves: loud non-zero failure on a simulated old Node without loading the ESM server, no-op handoff to runMcp() on the supported Node the suite runs on, and parse-safety of the emitted shim. (packages/flair-mcp/*)

🛟 Harper watchdog now recovers an UNLOADED launchd job + alerts on state transitions (ops-6nv7)

On 2026-06-27 ~04:20 prod Flair (:9926) was down — the ai.tpsdev.flair launchd job wasn't loaded (no Harper PID) — and it stayed down, undetected, until a memory write happened to fail. Two gaps: (1) harper-watchdog.sh only handled the PID-alive-but-/Health-dead zombie case (kill -9 + launchctl kickstart -k); kickstart/start are no-ops on an unloaded job, so the job-unloaded failure mode went unrecovered. (2) There was no alerting at all — a Flair-down was invisible. Recovery was a manual launchctl load ~/Library/LaunchAgents/ai.tpsdev.flair.plist.

The watchdog now recovers both failure modes and makes the event known:

  • Unloaded-job recovery. When /Health fails, the watchdog now distinguishes by pgrep harper.js + launchctl print gui/$(id -u)/<label> (with a launchctl list fallback). PID-alive → the existing zombie path (kill -9 + kickstart -k). No PID + job loaded → nudge with kickstart -k. No PID + job unloaded (the incident)launchctl bootstrap gui/$(id -u) <plist> with a launchctl load fallback — the operation that actually reloads an unloaded job.
  • State-transition alerting (non-spammy). A small up/down state file (~/.tps/state/harper-watchdog.state) gates alerts so they fire on transitions (down→recovered, or first failure-to-recover), not every 60s tick. Alert channel preference, reusing the house pattern from mail-deliver-health.sh / mail-loop-canary.sh: Discord webhook (~/.tps/secrets/discord-webhook-tps-activity, #tps-activity) → tps mail send flint fallbac...
Read more