Skip to content

v2.32.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 00:39
· 185 commits to main since this release
aee453e

Frontend toolchain: Tailwind v4, ESLint 9, Next 16 (#188, #186, #189)

  • 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.
  • Next.js upgraded from 15.5 to 16.3.

mcp constraint widened to <3 (#184)

  • 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.

The backend has a type gate again (#195)

  • 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.

The process exposes Prometheus metrics (#145)

  • 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.

The supply chain is pinned and audited (#146)

  • 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.