You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tailwind CSS upgraded to v4: PostCSS plugin moved to @tailwindcss/postcss, @tailwind directives replaced with @import "tailwindcss" + @config to
preserve the existing shadcn-style TS theme; autoprefixer dropped (built-in).
ESLint upgraded to v9 with eslint-config-next v16; legacy .eslintrc.json
replaced by flat eslint.config.mjs. The one genuine react-hooks/immutability
finding (agents page) was fixed; the 23 set-state-in-effect findings are
disabled pending a dedicated refactor of the prop-to-state sync idiom.
mcp>=1.0,<2 broadened to mcp>=1.0,<3 in pyproject.toml; uv.lock
regenerated so uv lock --check passes. The locked mcp version is unchanged.
Next 16 regressions fixed: lint gate and themeColor (#256, #257)
next lint was removed in Next 16; next build no longer ran ESLint. Added
an explicit npm run lint CI step and a lint script to package.json.
themeColor moved from the metadata export to viewport as required by
Next 16; <meta name="theme-color"> is now present in the static output again.
The API surface is versioned and frozen as a contract (#193)
The same handlers are now served under /api/v1/..., and the versioned
paths are the published contract: they are what openapi.json freezes and
what CI checks for drift. The bare /api/... paths keep working as a
compatibility alias but are hidden from the schema, so a client can at last
tell a compatible edit from a breaking one. /api/health stays unversioned
by design.
mypy now runs as a CI job over the tier of modules that is annotation-clean
today ([tool.mypy].files in pyproject.toml). The list is a ratchet: widen
it as modules are fixed. This restores #147's deferred type-check item,
which #176 left without a follow-up.
The generated API docs follow the token gate (#144)
/docs, /redoc and /openapi.json were served without a token even when LEVH_TOKEN was set, because the gate only covered the /api/ prefix — an
authenticated deployment handed any anonymous caller a complete map of all
108 routes. A browser cannot attach X-LEVH-Token to the /docs document
request, so the surface is now withheld (404) while a token is in force, and
restored deliberately with LEVH_ENABLE_API_DOCS=true on a trusted network.
GET /api/metrics (and /api/v1/metrics) publishes the in-process registry
in text exposition format: recall/store latency histograms, the embedder
fallback and derived-rebuild counters, and the admission-verdict
distribution. The Docker HEALTHCHECK scrapes it as well, so a process whose
metrics surface is down no longer reads as healthy.
Every request gets a request_id — reused from X-Request-ID when it is a
sane alnum/-_.: string under 128 chars, generated otherwise, echoed in the
response header — and it is injected into each log record, which is what lets
the recall → admission → store lines be stitched back together. With LEVH_LOG_JSON=1 the logs are emitted one JSON object per line instead of the
human-readable format.
GET /api/readyz separates readiness from liveness: it pings SQLite, reports
the live embedder mode and whether derived state is behind, and answers 503
with the reasons when it is not ready.
uv.lock pins the full backend graph and CI installs from it with uv sync --frozen, so one commit no longer resolves to a different set of
packages weeks later; uv lock --check fails the build on a stale lock. The
audit follows the lock too: pip-audit runs against uv export output rather
than whatever pip happened to resolve that day, and Dependabot keeps both the
lock and the npm graph current.
Releases now carry CycloneDX SBOMs for the Python and frontend artifacts, and
a sast job runs Bandit at Medium+ so a new finding cannot land under the
gate unnoticed.
Hook installers no longer promise a checkpoint they cannot install (#124)
install_claude_code_hook and install_universal_hook accepted a with_checkpoint argument and silently dropped it: nothing read the
value, the CLI never defined --with-checkpoint so the path was
unreachable, and _CHECKPOINT_TEMPLATE had no callers. The dead
parameter and the unused template are gone, and the module docstring no
longer advertises a periodic-checkpoint capability these hooks do not
provide. Recurring checkpoints remain available via levh checkpoint auto.
A row the model cannot read no longer takes the store down (#267, #273)
A store row the model rejects — an outside agent writing memory_type="long_term", or a NULL id that SQLite allows on a non-INTEGER
primary key — used to abort every startup: EpisodicMemory.get_all() raised
out of MemoryEngine.initialize() and every other memory became unreachable.
Such rows are now quarantined: skipped with a warning that names the row, so
the store stays readable.
The store now enforces the model's own contract on INSERT and UPDATE through
triggers generated from one rules table in the schema module (id, content,
memory_type, importance, frequency, timestamps). A rejected write names the
column and what the model needs, and the store file is left untouched.
A failed startup no longer leaves the process alive forever: the engine is
published before initialize() and shut down in every outcome, so levh serve exits non-zero on startup failure instead of hanging on the
open aiosqlite worker thread.
Quarantined rows are visible, and doctor counts them the way the read path does (#270, #272)
Every quarantine increments levh_memory_rows_quarantined_total in the
Prometheus registry, and levh doctor fails on any row the current model
cannot accept — so a store quietly rotting under an external writer produces
a standing signal instead of passing a health review silently.
The doctor count now judges rows by meaning rather than storage shape: metadata and tags are NULL when never set, and reading those columns raw
made every sparse row look invalid (2 genuinely unreachable rows read as 18).
The storage-shape-to-model conversion lives in one place — server.core.db.memories.row_to_memory_dict — shared by the query layer and
the doctor check, with an equivalence test pinning the two together.
levh doctor inspects SQLite from a running event loop, so the check no
longer trips on the async driver it is reading through (#271).
Structured startup and static-export polish (#284, #268)
levh serve printed its startup banner with bare print(), so LEVH_LOG_JSON=1 did not make it structured even though logging.py
advertised that path. It now routes through emit() — verbatim when JSON is
off, a JSON record when on — and the stale docstrings describe what emit()
is actually for (#284).
The static-export logo opts out of no-img-element explicitly, so the
frontend lint gate stays green without a blanket rule change (#268).
Dependency and CI maintenance
Dependabot bumps across the frontend graph (vitest 2 → 5 with a vite 6.4
floor, jsdom 25 → 30, recharts 2 → 3, @types/node 20 → 26, lucide-react, @testing-library/jest-dom) and the workflow graph (actions/setup-node, actions/setup-python, actions/upload-pages-artifact, astral-sh/setup-uv), plus dependabot rules that stop it opening major
TypeScript and ESLint bumps that would need a migration of their own.