Releases: tpsdev-ai/flair
Release list
v0.16.0
🧪 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_modulespackage dir), creates a non-rootflairuser, and runsflair init+ the daemon as that user with NOFLAIR_MODELS_DIRoverride — 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 →EACCESon download → DEGRADED. - The assertion is genuine semantic recall, not keyword match. The gate asserts
flair initreportsSemantic search operational(the #533 in-init check, which printsDEGRADEDbut does not exit non-zero), then runsflair doctoras the hard gate —doctorperforms 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) andprocess.exit(1)on degraded. Keyword-only fallback cannot satisfy it. Embeddings dead → the gate FAILS. - Wired into
.github/workflows/docker-test.ymlas a newclean-vm-gatejob 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). NewresolveModelsDir()resolves, in order:FLAIR_MODELS_DIR(explicit override) →<ROOTPATH>/models(Harper's data dir — Flair passesROOTPATH = ~/.flair/datawhen it spawns Harper, so this is user-owned and writable even under a root-owned install) →<cwd>/modelsonly 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~/.flairand the package dir stays read-only.FLAIR_MODELS_DIR(already used bydocker/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>/modelsdefault, the integration harness (fresh tempinstallDirperstartHarper) would otherwise re-download the ~80MB model every run (HuggingFace 429-prone, #463/#465). The harness now setsFLAIR_MODELS_DIRto the repo-rootmodels/that CI/local pre-download into; a pre-existing parentFLAIR_MODELS_DIRstill wins. - New unit coverage (
test/unit/embeddings-models-dir.test.ts) asserts the resolution order, including the load-bearing invariant: a fresh install with noROOTPATHand 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-mcpbin now points at a CommonJS preflight shim (dist/mcp-shim.cjs, compiled fromsrc/mcp-shim.cts). CJS evaluates top-to-bottom with lazyimport(), 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 torunMcp(). - 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 --checkconfirms parse-safety. src/index.tsnow exportsrunMcp()— all runtime side effects (theFLAIR_AGENT_IDcheck,FlairClientconstruction, 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 untilrunMcp()is called. Direct invocation (node dist/index.js,bun src/index.ts) still works via animport.meta.mainentry-point guard.engines.nodebumped>=18→>=22to match flair's CLI and the deps' real floor, sonpm installalso warns on an unsupported Node. Postinstall nowchmod +xthe 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 torunMcp()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
/Healthfails, the watchdog now distinguishes bypgrep harper.js+launchctl print gui/$(id -u)/<label>(with alaunchctl listfallback). PID-alive → the existing zombie path (kill -9+kickstart -k). No PID + job loaded → nudge withkickstart -k. No PID + job unloaded (the incident) →launchctl bootstrap gui/$(id -u) <plist>with alaunchctl loadfallback — the operation that actually reloads an unloaded job. - State-transition alerting (non-spammy). A small
up/downstate 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 frommail-deliver-health.sh/mail-loop-canary.sh: Discord webhook (~/.tps/secrets/discord-webhook-tps-activity, #tps-activity) →tps mail send flintfallbac...