Skip to content

v0.7.0

Latest

Choose a tag to compare

@Magnus-Gille Magnus-Gille released this 24 Oct 17:07
· 4 commits to main since this release
242c519

Kvalitetsomgång #396: 13 verifierade rättelser, inklusive auktorisering av klassificeringstaket, OAuth-återkallelse och consent, skanning efter hemligheter, migrering 32, kontrollerad nedstängning och keep-alive bakom cloudflared (#404). Dessutom ingår det core-first-experiment och de evidence-verktyg som mergats sedan 0.6.1.

Uppgradering: schemat går från v31 till v32. Migreringen ändrar bara kolumnlistan i FTS-triggern och bygger inte om något. En återgång till 0.6.x-kod fungerar mot en databas på v32. Integrations-PR: #417.

Added

  • A core-first whole-message experiment (#342) tests optional neighbor expansion
    after admitting query-matched messages at the same 8,192 estimated tokens.
    It retains paired comparisons against the frozen baseline, passage packing
    and mandatory-neighbor packing, including the prior 29-question regression
    cohort. Selection stays label-free; default packing is unchanged.

  • An opt-in whole-message packing experiment (#342) selects a query-matched
    message together with its immediate neighbors under the unchanged 8,192
    estimated-token budget. Label-free, hash-bound source boundaries preserve
    complete messages, and paired reports compare the candidate with both the
    frozen baseline and the prior passage experiment. Default packing is unchanged.

  • An opt-in query-based passage-packing experiment (#342) compares verbatim text
    slices with the frozen #341 control at the same 8,192 estimated-token budget.
    It reports paired gains and losses in entry-ID presence and complete annotated
    message coverage, with source offsets and an explicitly labelled subset.
    Public dataset annotations are reproducibly derived and never enter passage
    selection. Default packing and the frozen baseline remain unchanged.

  • An opt-in fixed-budget context-packing comparison (#342) reuses the frozen
    #341 retrieval baseline at 8,192 estimated tokens. It compares legacy packing
    with skipping oversized entries, reports retrieval/packing stages and paired
    gold-ID gains/losses, and requires exact control reproduction. Default packing,
    baseline bytes and the existing regression policy remain unchanged.

  • A cold-build command for the 500-question LongMemEval-S evidence lane (#341)
    creates a new snapshot and frozen query vectors from a pinned public dataset
    and BGE model. Its manifest records model-file, input, output, source and runtime
    provenance; the existing evaluation command replays those artifacts offline.

  • A frozen LongMemEval-S packed entry-ID baseline (#341) records a new 297/500
    (59.4%) measurement with exact per-question rows, portable provenance and an
    upstream model-file verification receipt. Large inputs remain in local ignored
    storage with hash-checked offline replay instructions. This does not reproduce
    the historical 296/500 result or claim an attributable improvement.

  • An offline packed-gold entry-ID benchmark lane (#341) uses explicitly supplied
    detached snapshots and frozen query vectors with immutable embedding provenance.
    It records context truncation, input/source hashes and paired all-gold question
    changes, requiring a reproduced regression before exit 1. Initial candidates
    remain not_measured (exit 2) until compared with a reviewed baseline; that
    initial state is preserved in the new frozen baseline's original report bytes.

  • Published LongMemEval-S benchmark inputs in the versioned, non-product GitHub
    release benchmark-evidence-longmemeval-s-20261018. Its clean-checkout
    checker verifies pinned sizes, hashes and per-file provenance, downloads or
    reuses the five inputs, then compares offline and writes a verification
    receipt. The CI workflow definition covers PRs/pushes to main, daily
    04:23 UTC, and manual dispatch with 30-day receipts. The schedule starts
    after merge. The lane never updates the baseline.

Security

  • memory_write (patches and full overwrites) and memory_update_status
    (including validate_only previews) now check the existing entry against the
    caller's classification ceiling, using the same predicate as memory_read,
    before merging, previewing, or changing its classification (#406). Previously
    a principal with write access but a lower ceiling could lower a hidden entry's
    classification with a tag-only patch, overwrite and declassify it with a full
    write, learn its revision from a CAS or create_if_absent conflict, or see
    protected status content in a preview. Such calls are now denied as hidden
    targets (access_denied for agents, {"found": false} otherwise,
    classification_error for owners) and modify nothing. The check is repeated
    inside the write transaction, so a concurrent classification raise from
    another connection cannot be overwritten. Callers whose ceiling covers the
    entry are unaffected.

  • Production dependencies are lifted past four advisories (#236): body-parser
    2.4.0 (GHSA-27r3-jxmh-p828), zod 4.6.7 (GHSA-mrhx-c7xc-m3w5), fast-uri
    3.1.9 and ip-address 10.7.4. The overrides floors are raised so the lockfile
    cannot fall back. npm audit reports no advisories. The previously tracked
    Hono and Sharp advisories no longer apply to the resolved versions.

  • OAuth consent identity binding (#411). The resolved consent identity is now
    bound per request (res.locals.consentIdentity) instead of through a shared
    provider field. Previously two concurrent POST /authorize requests from
    different principals could cross: one pending authorization received the other
    principal's identity and the other was left unbound. Approval re-resolved the
    identity so there was no escalation, but the GET/POST-time binding of the
    consent TOCTOU defence was not reliable. setLastResolvedIdentity is removed.

  • OAuth token lifecycle (#408). Revoking an OAuth access or refresh token via
    the revocation endpoint now revokes the whole access/refresh pair, so a client
    "disconnect" no longer leaves a 30-day refresh token (or a live access token)
    behind. Refresh rotation now revokes the previous access token. A refresh
    request may only narrow the originally granted scopes; requesting scopes outside
    the grant is rejected with invalid_scope instead of being issued. Expected
    grant failures (unknown, replayed, expired or foreign refresh tokens and
    authorization codes, redirect mismatch) now return HTTP 400 invalid_grant
    instead of HTTP 500 server_error. A rejected refresh request no longer
    consumes the refresh token.

  • OAuth admin revocation (#407). oauth-clients clear <principal> now revokes
    every OAuth access and refresh token bound to the principal and invalidates its
    unredeemed authorization codes, even when the principal-client mapping belongs to
    another principal (shared public client_id). oauth-clients remove <client> now
    also invalidates the client's approved-but-unredeemed authorization codes, and
    code and refresh-token exchange fail closed with invalid_grant when the client
    is no longer registered. clearOAuthClients returns
    { mappingsRemoved, tokensRevoked, codesInvalidated }; the clear text output
    reports all three counts and its --json output adds tokensRevoked and
    codesInvalidated (removed still counts mappings). Clearing a principal no
    longer revokes other principals' tokens on a client it happened to own.
    removeOAuthClient likewise returns { removed, mappingsRemoved, tokensRevoked, codesInvalidated }, and oauth-clients remove prints those counts in text and
    --json. Authorization-code and refresh-token exchange and both admin commands
    now run as IMMEDIATE transactions (code claim, validation and token issuance are
    one transaction), so an admin revocation from the separate CLI process can no
    longer land between a code being claimed and its tokens being issued.

  • The secret scan now also rejects credential formats it previously let through
    (#410): private-key PEM blocks of any algorithm (EC, DSA, OPENSSH,
    ENCRYPTED, ... not just RSA/plain), JSON Web Tokens, Slack tokens
    (xox?-, xapp-), GitLab PATs (glpat-), Stripe live/restricted keys
    (sk_live_, rk_live_), npm tokens (npm_), Google OAuth access tokens
    (ya29.) and lowercase bearer <token>. All patterns require a minimum body
    length so prose such as "sk_live" in docs or a short npm_install tag is not
    flagged. redactSecrets (error and telemetry masking) uses the same list and
    now also masks the key material of a PEM private-key block, not just its
    BEGIN line.

  • Credential-shaped values in stored metadata and retrieval telemetry are now
    handled like credentials in content (#409). memory_write (including
    patch.tags_add), memory_log and memory_update_status scan namespace, key and
    tags with the same secret patterns and reject with the same
    validation_error contract (Tag|Key|Namespace appears to contain a secret or credential (matched pattern: ...)), never echoing the value; the scan runs
    before format validation so malformed secret-shaped values are not echoed
    either. memory_query and memory_retrieval_feedback mask secret matches as
    [REDACTED] (deterministic, from the shared pattern list) before persisting
    retrieval_events.query_text / retrieval_feedback.query_text, and pagination
    snapshots store masked query, namespace and tag filters. Searching uses the
    caller's original text; cursor resume compares secret-shaped arguments by a
    keyed digest (HMAC-SHA256 under a random per-process key that is never
    persisted), so a different secret is never accepted as a continuation. After a
    restart, or for snapshots created before this change, a secret-bearing
    argument cannot be verified and the client is told to resume with the cursor
    only. memory_query responses echo the masked query on every page, and memory_read_batch failure items mask
    credential-shaped namespace/key values. Namespace and key format errors redact
    credential-shaped values, and memory_update_status rejects a secret-bearing
    namespace before any format error. Historical rows are not
    rewritten; scrubbing existing retrieval_events.query_text is a separate
    operator decision.

  • Keep offsite backup and snapshot Heimdall bearer tokens out of curl arguments,
    and reject tokens containing CR/LF before sending a panel update.

  • OAuth consent proxy and identity gates. Public issuers now require an exact
    trusted proxy header/value match on both consent endpoints; an identity email
    cannot satisfy that gate. When identity mapping is configured, missing or
    unresolved identities fail closed, and loopback owner fallback is limited to
    direct loopback requests for local issuers.

  • Prevent classification laundering through consolidation. Snapshot the namespace floor and all log, current status, and prior synthesis classifications before prompt construction; withhold any run above internal before contacting a provider, regardless of custom endpoint URL, and enforce the requester's ceiling separately. Persist synthesis at the maximum input classification, ignore model-proposed classification tags, reject stale snapshots after async calls and preview confirmation, and suppress cross-references when their source data exceeds the source namespace floor. The background worker treats this policy skip as ineligible input rather than an API failure.

  • Redact HTTP error logs. Keep failed /mcp request logs to structured
    attribution and rate-limit metadata; do not retain or serialize request
    bodies or arbitrary request headers in diagnostics.

Fixed

  • Graceful shutdown (#413) now drains in-flight HTTP requests before closing the
    database: it stops accepting connections, closes idle keep-alive sockets, waits
    up to 10 s for active requests, then force-closes any that remain. Previously
    SIGTERM closed the DB and exited immediately, cutting requests off on deploy
    restarts. The embedding and consolidation workers are now stopped
    independently, so a rejecting embedding stop (for example an in-flight batch
    whose failure-status write throws) no longer skips the consolidation stop. The
    worker-stop phase is bounded to 20 s in total (a stuck in-flight LLM call is
    abandoned), keeping a restart under systemd's 90 s stop timeout; the exit code
    is 1 if any step failed or timed out. Both workers also no longer re-arm a
    timer after stop, and their timers are unref'd.

  • The HTTP server now keeps idle connections for 95 s instead of Node's 5 s
    default, longer than cloudflared's 90 s origin pool (#404). A proxy reusing a
    socket Node was closing got a connection reset, which failed Claude's OAuth
    client registration (POST /register) and caused intermittent origin 502s.

  • Migration 32 (#412) restricts the entries_au FTS trigger to
    AFTER UPDATE OF content, namespace, key, tags, rowid. Embedding-worker
    writes to embedding_status and other non-indexed columns no longer delete and
    re-insert the FTS row or re-run munin_split_tokens over the full content.
    Trigger body and code-health exclusion logic are unchanged and the FTS index
    is not rebuilt. Rollback is a new forward migration recreating the trigger
    without the OF list.

  • Benchmark corpus generation now rebuilds vectors with missing or stale immutable
    identities, records the active identity, and rejects model-binding changes.

  • Bind stored and query embeddings to an immutable model/runtime identity, including
    revision and effective dtype, rather than the model name alone. Migration 31
    leaves legacy identities unknown; the worker gradually rebuilds incompatible
    vectors without changing human memory or its timestamps. Health reports compatible
    coverage and backlog; a read-only database preview supports rollout planning.
    Custom embedding models now require MUNIN_EMBEDDINGS_REVISION; missing or invalid
    revisions disable optional embeddings and preserve explicit lexical fallback.
    Model IDs must use Hugging Face owner/repository syntax. Unversioned local
    directories and URLs are no longer accepted; offline reloads require the pinned
    revision in the filesystem cache.

  • Relaxed lexical fallback keeps non-ASCII words whole (#334). When a strict
    query matches nothing, the OR fallback now splits on anything that is not a
    Unicode letter, number, _ or -, so words such as återställning or
    Björkro are no longer broken into unmatchable fragments. Hyphenated and
    underscored compounds also contribute their parts (Jev-integration also
    searches jev and integration), common Swedish function words are ignored,
    and the explain-reason and exact-anchor term matching use the same Unicode-aware
    terms.

  • Equal hybrid fusion scores are ordered by relevance, not by entry id (#340).
    Ties in the reciprocal-rank-fusion score now break by lexical rank, then
    semantic rank (an entry missing from a leg sorts after one present in it), and
    only then by entry id, so the order stays fully deterministic. memory_query
    and the benchmark runner share one comparator.

  • memory_query no longer fails on broad candidate sets and keeps relevance near the top (#346).
    Since #306 any retrieval leg with more than 500 candidates returned
    query_bound_exceeded. It failed whenever a leg held more than 500 candidates,
    which on a store with more than 500 embedded entries in scope meant unscoped
    hybrid and semantic queries, and natural-language lexical queries through the
    relaxed fallback. Each leg,
    the fused hybrid set and the final set now keep their best 500 in retrieval
    order; the response carries retrieval.candidates_truncated: true and
    retrieval.candidate_cap: 500 on every page (omitted when nothing was dropped),
    and total_matched is then a lower bound. The query_bound_exceeded error no
    longer exists. Structural reranking (tracked-status class, entry type, recency)
    is again limited to the 50 best-relevance candidates (QUERY_RERANK_WINDOW), as
    it was before #306, so a weak match can no longer be lifted onto the first page
    from far down the candidate list; results after the window keep retrieval order.
    Inside the window structural class still outranks relevance (#335).

  • Principal provisioning and query-snapshot eviction reject or isolate blank IDs (#268). addPrincipal now rejects blank and whitespace-only IDs at the exported provisioning boundary, while legacy/manual snapshots whose principal ID is the empty string still use an exact per-principal capacity query and cannot evict another principal's cursor.

  • memory_review preview now reports approval write effects truthfully (#272).
    Preview previously dry-ran terminal status, validation, source freshness, and
    target freshness through separate logic from the real approve path, so
    purged-terminal previews could collapse to payload_expired, preview catch
    paths could skip internal-error telemetry, and the exact approval outcome
    could drift across duplicate, expired, failed, or superseded proposals.
    Preview and approve now share one precondition evaluation, the same inclusive
    expiry boundary, and the same retention masking rules. Successful previews
    stay pure-read and metering-free; unexpected preview exceptions still emit
    internal_error telemetry. Terminal proposals whose payload has already been
    purged still return the truthful non-writing outcome instead of claiming the
    payload state blocked the answer. A real approval attempted after expiry still
    returns the same machine-readable review_expired code and message
    advertised by preview through the stable ok:false rejection envelope.
    Non-terminal approve-time rejections now run through the transactional
    approval path, append one durable approval_conflict event per rejected
    attempt, and restore the indexed expires_at <= ? prune range instead of
    scanning every pending or edited proposal row.

  • Commitment-derived views no longer let resolved rows consume the page cap (#274).
    memory_commitments now paginates the SQL-eligible open and recently-done rows
    to exhaustion before filling its independent output buckets, while
    memory_patterns and memory_handoff prefilter to open rows. Cancelled and
    old completed rows therefore cannot hide current obligations, and the
    recent-completion cutoff is shared by query eligibility and classification.

  • memory_commitments now recognizes future-dated verify/run forms and
    explains conservative exclusions (#274).
    The dated-action vocabulary now
    includes verify, so future phrases such as We will verify ... by YYYY-MM-DD and I will run ... on YYYY-MM-DD surface alongside existing
    validate/check/test forms. The tool now also returns bounded,
    content-blind exclusion_diagnostics when commitment-like syntax is matched
    but deliberately dropped — for example retrospective dated logs, duplicates
    inside one entry, terminal/resolved sources, or legacy plain-markdown status
    blobs that use an ad-hoc Next Steps: heading. Public wording now matches the
    real contract: canonical tracked-status Next Steps, dated future clauses in
    visible tracked-status prose, explicit memory_log commitment phrases such as
    We agreed to: ... or I commit to: ..., and future-dated memory_log
    phrases can surface here. Generic non-status state fields are not commitment
    sources. Legacy plain markdown status blobs with ad-hoc Next Steps:
    headings remain readable but are not commitment-eligible until migrated to the
    canonical structure. Refresh now keeps the compatibility boundary honest too:
    legacy rows from those plain-status Next Steps: blocks retire through a
    non-completion path, and older whole-segment dated rows revise in place to a
    surviving future clause instead of reading as completed just because the
    derived fingerprint changed. The parser now also keeps due dates local to the
    actual future clause, so a retrospective date such as completed ... on 2026-07-31 cannot be recycled into a new overdue commitment when a later
    noun-subject clause says the report should be filed later. Legacy plain
    Next Steps: blocks are stripped by the same structural matcher that powers
    the exclusion diagnostics, which fixes three drift cases at once: a legacy
    block before the first ## heading no longer leaks commitments through the
    preserved prefix, line-mode stripping stops at the real end of the block
    instead of consuming unrelated later prose, and canonical extras headings
    such as ## TODO / ## Action Items stay visible for dated obligation prose
    even when another section contains a Status: line. Inline canonical
    **Next Steps**: lines now normalize to the same single tracked-next-step
    commitment as the ## Next Steps form, and empty-namespace diagnostics use
    the same exact-or-subtree namespace scope semantics as the derivation scan.

  • Write-like responses now return only the settled stored classification
    (#263).
    Investigation confirmed this branch resolves and persists entry
    classification synchronously before the write response and audit row are
    emitted, and no per-entry pending/settlement path exists. memory_write,
    memory_update_status, and memory_log therefore no longer emit the fake
    classification_provisional field, and audit rows no longer stamp a
    provisional classification suffix. The response, stored entry, and
    memory_history now report the same effective classification without
    pretending an asynchronous settlement flow exists. Optional background work
    remains non-blocking.

  • Namespace subtree filters are now literal and case-sensitive across query paths (#267).
    The shared SQL matcher behind memory_query, semantic exact-namespace scans,
    audit history, commitments, and adjacent namespace-filtered reads previously
    used SQLite LIKE, which is ASCII case-insensitive by default and could drift
    from the server's case-sensitive startsWith checks. Namespace subtree
    filters now use exact equality plus a UTF-8-safe literal descendant range, so
    supplementary Unicode descendants stay inside scope, Projects/Foo no longer
    matches projects/foo/..., trailing-slash filters remain descendant-only, and
    the shared SQL helper treats wildcard-looking bytes literally instead of as
    LIKE wildcards. Direct DB/helper coverage also verifies already-stored
    legacy % bytes without expanding the public namespace-filter contract. For
    memory_query, non-owner reads now apply readable-namespace SQL selectors
    before LIMIT only when the caller's read rules are exactly representable;
    legacy or unsupported rule shapes fail open to broader SQL scanning while
    canonical post-query authorization remains authoritative. memory_query now
    reports namespace_scope: "subtree" for bare namespace filters,
    namespace_scope: "prefix" for trailing-slash filters, and omits the field
    when no real namespace filter was applied.

  • memory_read(as_of) no longer leaks hidden history and ordinary rewrites now advance their own time boundary strictly (#273). The tool description promised the state revision valid at any past instant, but ordinary overwrites and patches mutate the current row in place and migration v20 backfilled legacy state valid_from from the last updated_at, so uncovered times could return found:false with a contradictory hint listing the same key. Worse, the miss-coverage probe reasoned over raw rows, so a caller below the classification ceiling could learn that hidden history or a hidden current row existed via history_available:false or the "Read the current entry" hint. memory_read(as_of) now treats explicit correction lineage as the only rewindable history and only reports uncovered-gap metadata when the caller is authorized to know that recorded gap exists at all; otherwise the result stays indistinguishable from an ordinary miss while still listing any visible sibling keys. Ordinary in-place state rewrites also advance valid_from strictly past the prior row boundary even when two writes land in the same millisecond, so past as_of reads cannot return rewritten current content. Pre-creation timestamps remain ordinary misses. The exact visible current valid_from/updated_at boundary can be round-tripped even when a same-millisecond monotonic timestamp is narrowly ahead of the server clock; arbitrary future timestamps and hidden future boundaries remain rejected. Explicit supersedes chains keep their half-open boundary semantics.

  • memory_delete previews disclose and bind the full correction lineage
    they will remove (#281).
    A delete of a corrected state entry removes its
    current revision and superseded revisions, but the preview previously
    reported and fingerprinted only the current row. state_count now includes
    every state revision that confirmation can delete, with
    current_state_count and historical_state_count making that scope explicit;
    the token fingerprint covers the same complete set. A classification- or
    owner-scoped delete that would cut through a correction chain now fails
    closed with partial_lineage before it issues a token, rather than presenting
    a misleading partial preview and failing only at confirmation.

  • memory_delete preview tokens are bound to the entries they previewed
    (#266).
    A token carried only {namespace, key, expiresAt}, so a preview
    taken before an update still authorised the delete afterwards: preview an
    entry, let anything rewrite it, confirm with the old token, and the newer
    revision the caller never saw was destroyed with ok:true and no warning.
    Found by an independent user-test agent that reproduced it live against
    production. The preview now computes a digest over the id, updated_at and
    content of every row in scope — content included because updated_at has
    only millisecond resolution, and a same-millisecond rewrite would otherwise
    be invisible — and confirmation recomputes it from the same queries. A
    mismatch returns the new preview_stale error with the previewed and current
    counts, and spends the token so a fresh preview is required. This covers the
    namespace-wide path too: an entry appearing in the namespace after the
    preview now blocks the confirm rather than being swept up silently.

    Cross-model review tightened two details before merge. The digest covers
    tags, classification and valid_until as well as content, because a
    metadata-only patch (say tags_add) sharing a millisecond with the preview
    would otherwise leave the fingerprint unchanged. And the comparison now runs
    inside the delete transaction rather than just before it, so a second
    connection committing between check and DELETE cannot reopen the same
    window.

  • A reworded next step no longer reads as a completed commitment (#253). A
    commitment's identity was its normalized text, so editing a ## Next Steps
    line changed its fingerprint. Reconciliation then resolved the old row
    done while inserting the reworded successor as open, so one live item
    appeared in both buckets. An unambiguous, meaningfully similar rewording now
    revises the existing row in place; issue references constrain matches but
    cannot alone identify a step, and different non-empty reference sets never
    pair. A genuinely removed step still resolves.

  • Bridge reports the real cause of network failures and retries only read-only requests (#321).
    When a forwarded request fails with a thrown network error the bridge error now
    names the cause in a sanitised summary: an error code, an allowlisted syscall and
    a fixed description from a constant table instead of only "fetch failed". The
    summary excludes cause-message free text; original SDK, HTTP and body error
    messages can still be retained. Only read-only requests (non-tools/call messages
    and an allowlist of read tools) are retried after thrown network failures, up to
    twice; writes, unknown tools, batches and unparseable bodies are not retried after
    those failures. The existing server-marked admission-v1 429 retry remains because
    it rejects before execution; unmarked 429 responses are still returned without
    replay. A failed write says what is known: most likely not applied when every error
    in the chain proves a connect-phase failure, otherwise that it may or may not have
    been applied; in both cases check the current state before repeating it. A write
    whose JSON response body cannot be read, or which receives an HTTP error, gets the
    same guidance and the request id. Asynchronous SSE failures remain outside this
    guidance. The bridge's own timeout is never retried. Each
    forwarded request carries a random X-Munin-Request-Id header, reused across
    retries and shown in bridge errors and network retry and failure stderr logs;
    the server logs it as requestId
    in the request log line only when it matches [A-Za-z0-9_-]{8,64}, and does not
    use it for anything else. scripts/stdio-bridge.mjs gains the cause reporting
    only (identical table, tested).

Changed

  • Report bounded intake sample completeness (#367). Read-authorized memory_write and
    memory_log reports now expose the number of visible entries analyzed, the 100-entry cap,
    and whether eligible visible entries were omitted. Tag-vocabulary findings describe the
    evaluated sample and identify incomplete samples; write-only calls omit these fields.

  • Node.js runtime support now starts at 22, with Node 24 LTS preferred (#365).
    The package engine, quick-start preflight, documentation, and CI now reject
    Node 20/21 with an actionable upgrade message while continuing to accept
    newer Node releases. CI covers Node 22 and 24 plus the native ARM64 Node 24
    quick-start path and real SQLite/vector/embedding inference with cache-only
    reloads; dependency versions are unchanged.

  • Successful memory_write patches return their new updated_at (#278, item 2).
    Callers can immediately use the patch response timestamp as the expected version
    for their next CAS write. Conflict and not-found responses keep their existing shape.

  • Benchmark production-query parity (#343). production_ranker now uses
    the same 500-candidate cap per retrieval leg, semantic-distance cutoff and
    default expiry filtering as memory_query. It also mirrors the 50-candidate
    rerank window, suppression and exact-anchor uniqueness checks across the
    remaining candidates. Raw benchmark retrieval retains its existing behavior.

  • memory_query ordering stays anchored to relevance (#335, #248). Inside the
    50-candidate rerank window, each entry's ranking position is now adjusted by a bounded
    amount (structural class: up to five positions' worth of lift or one of demotion;
    recency: in proportion to search_recency_weight, about two positions' worth at the
    default) and entries are then ordered by the adjusted position, so an entry's final
    place also depends on how its neighbours were adjusted. This replaces structural class
    and newest-first overriding relevance. Entries whose structural score is at or below the
    demotion threshold (a plain tombstone, for example) sort after all others; a tombstone
    that also carries a strong structural boost can stay above it. Recency is computed from
    updated_at values only, never the clock; explain freshness_score remains
    informational (age relative to now). Broad orientation and attention-triage queries keep
    the structural-first ordering, with newest-first among equal structural scores when
    search_recency_weight is above 0. The reranker also normalises the weight itself
    (non-finite falls back to the default, finite values clamp to 0..1) for callers that
    bypass handler validation.

  • memory_review proposals are isolated by creator session/run (#271). Durable
    proposals now retain a server-derived creator session, and list/get/preview/edit/
    approve/decline/undo actions default to the current session plus principal. A
    known foreign-session ID returns the same non-leaking not_found response as an
    unknown ID. scope:"principal" is an explicit same-principal cross-session
    opt-in; it never expands across principals. Lists support exact namespace and
    operation_type filters. Legacy rows with no session remain principal-scope-only
    until their normal retention lifecycle removes them. Authenticated OAuth and
    agent-token HTTP requests that omit Mcp-Session-Id receive a fresh random,
    signed server-issued run handle and must echo it for subsequent review actions.
    Handles are bound to the authenticated principal and credential; static bearer
    callers and requests with invalid presented handles fail closed, so neither can
    capture durable review proposals.

  • memory_query pagination snapshots are now bounded operational writes with authenticated opaque cursors (#268).
    Multi-page query results now persist a server-side snapshot only when more than one page is actually needed, with a documented 5-minute TTL instead of a long-lived cache entry. Active snapshots are capped per principal and globally, expired rows are cleaned via the indexed expires_at range during startup and periodic maintenance for both transports, and a caller may evict only its own oldest active snapshots before the server throws capacity_exceeded rather than deleting another principal's cursor. Expiry pruning, caller-owned capacity eviction, and snapshot creation are atomic, so a failed creation cannot discard existing cursors. Query cursors bind snapshot identity and frozen position with an HMAC under a per-row random secret; the current access shape is bound separately through exact versioned canonical JSON (stored in the legacy access_fingerprint column), with fixed field order and sorted namespace rules rather than password-style hashing. Resumes reject tampered, access-mismatched, expired, or out-of-range cursors safely while still allowing limit changes and exact partial override replays that normalize back to the stored request shape. Resumed pages keep the frozen membership/order and explanation scores/reasons against newer writes or later status changes but re-check current deletion, expiry, authorization, and redaction before returning each page, so total_matched, returned, and has_more stay truthful even when snapshot members disappear after page 1. Semantic total_matched is exact over currently retrievable active-model indexed candidates, while hybrid totals are exact over the lexical plus currently retrievable semantic union; entries without compatible embeddings remain eligible for hybrid lexical matches. Those mode contracts define retrieval candidates; server-policy canonical orientation and blocked/needs-attention injections become members of the final frozen result set and count in total_matched. Hybrid expired_filtered_count counts unique expired candidate IDs across both retrieval legs.

  • memory_history now exposes explicit sync bootstrap state alongside older-page navigation (#268).
    Cursorless newest-first pages now return sync_cursor, the newest visible audit id in that initial feed (or 0 when the feed is empty), so clients can bootstrap forward polling without guessing from older_cursor. older_cursor pages keep newest-first history browsing but now return sync_cursor: null, which makes the initial watermark contract explicit. Their has_newer flag is derived from a separate SQL-visible segment using the same namespace, time, action, and access filters, so arbitrary or changed-filter cursors and hidden rows cannot manufacture newer history. Ascending sync pages keep numeric cursor compatibility and continue to advance next_cursor, while also exposing the same value as sync_cursor; empty sync pages preserve the input cursor in both fields. next_cursor remains null on cursorless/older pages to avoid inviting the wrong direction, and the tool metadata now documents has_more as a direction alias for has_older or has_newer.

  • MCP tool metadata now provides server-level instructions and expressive input schemas (#257). The server publishes fallback-safe first-operation guidance, while tool schemas expose relevant grammar, examples, limits, and lifecycle vocabulary.

  • memory_query serialization metadata now describes reranked linear order (#252). linear preserves the result order after structural reranking; visible lexical, semantic, and hybrid retrieval signals may differ from a result's final position. boundary remains a display-only transform applied afterward, and retrieval analytics continue to record the linear order.

  • memory_update_status now supports non-mutating sandbox validation via validate_only: true (#275).
    The normal mutation path is unchanged: real memory_update_status writes still require one
    of the caller's tracked namespaces and still create the usual state/audit/commitment effects.
    The new dry-run mode executes the same namespace authorization, lifecycle normalization,
    classification floor/visibility checks, secret and parameter-markup scans, valid_until
    normalization, legacy-status guards, and CAS/create-vs-update preconditions, but it never
    writes an entry, audit row, commitment, retrieval outcome, intake row, redaction row,
    or access-denied telemetry. Ordinary content-blind tool-call telemetry remains enabled so
    dry-run use and failures stay observable. Successful dry runs return
    status: "validated", wrote: false,
    would_write: "create" | "update", and the normalized prospective metadata. Read-visible
    callers also receive the previewed content / structured_status; write-only callers get
    the same generic withholding notice used by the real mutation response.

  • memory_orient now has a total response budget, a beginner mode, and an
    explicit response marker (#277).
    The per-group dashboard caps from #254
    still left standard responses large enough to blow past host output limits
    on mature estates. memory_orient now applies a deterministic
    response_character_budget on the final JSON payload in every detail mode,
    preserving the core handshake and reporting every budget-driven omission or
    truncation in response_budget_meta.adjustments instead of failing
    silently. Every orient response also includes generated_at so callers can
    identify the specific handshake they are looking at without pretending it is
    a transactional snapshot across later memory_list calls. A new
    detail:"beginner" mode returns the mental model, starter tool index,
    three common workflows, and safe write examples with no live dashboard or
    namespace-estate data, while compact remains the default first call.

  • Manual memory_consolidate is now a reviewed, source-grounded flow (#270).
    The first targeted call returns a non-persisting preview and a short-lived,
    one-use confirmation token; only confirmation writes that exact reviewed
    payload. The token fingerprints the whole unincorporated log window, so a
    newer log makes confirmation stale rather than being silently consumed.
    Preview claims must carry source-log UUIDs and be verbatim excerpts
    of every cited log, so unsupported teams, deadlines, risks, plans, or named
    entities are rejected instead of becoming durable synthesis. The server
    renders source links itself and strips model-supplied lifecycle tags, so a
    consolidation cannot assert or overwrite human-maintained status truth.
    Manual invocations now require an eligible projects/* or clients/*
    namespace; testing/* and other ephemeral/untracked roots fail closed.
    The preview discloses model, actual duration, completion-token count, the
    historical duration average when available, and explicitly reports when no
    provider-pricing estimate is configured. The prior unscoped, immediate-write
    manual invocation is intentionally retired; the background worker remains
    separately configured and retains its existing operational behavior.

  • External backup requirements now have a public-safe, versioned v1 consumer
    contract (#235).
    The contract binds a stable logical near-site target,
    owner-overlay authentication boundary, capacity/write guarantees, SQLite
    snapshot cadence/timeout, and a measured transfer-window preflight. It fails
    closed for offline, stale, interrupted, timed-out, or relocated targets.
    Verified copy/integrity evidence is distinct from periodic representative
    restore evidence, and encrypted offsite backup remains independent. The
    published schema and fixtures contain no deployment endpoint, private path,
    or credential.

  • memory_review preview response shape changed (#272). The legacy
    writes_memory field is removed. Preview now reports its own side effects
    with preview_wrote_memory:false and the current approval consequence with
    approval_would_write_memory:true|false. When retention has already purged a
    terminal proposal payload, preview still returns the truthful non-writing
    terminal outcome but omits exact_operation because no retained payload
    remains to display.

Security

  • Safe MCP exception boundary (#368). Unexpected backend failures return a
    fixed internal_error message and an opaque error reference. Deliberate
    validation, access, CAS, capacity, and delete-preview errors keep their public
    contracts. Diagnostic output contains only server-owned stage labels and
    references; diagnostic and telemetry failures cannot mask tool results or
    break optional intake/health degradation. Code-health returned errors use the
    same typed boundary; consolidation preview/background failures and health
    alerts retain fixed guidance with an error reference instead of provider text.
    A failed write response can follow
    a committed write, so inspect state or use an existing replay receipt before
    retrying.

  • CodeQL action pinning (#366). Update both CodeQL workflow steps to the
    reviewed v4.38.3 commit and group CodeQL action updates in Dependabot. A
    workflow contract check rejects mutable or mismatched init/analyze pins.

  • Legacy bridge redirect containment (#373). Prevent the standalone
    scripts/stdio-bridge.mjs from forwarding Cloudflare credentials or MCP session
    IDs across origins on message POSTs or cleanup DELETEs. Follow at most five
    same-origin 307/308 redirects; reject method-changing redirects, URL userinfo
    and invalid targets with fixed diagnostics. Legacy configurations using
    301/302/303 redirects must point MUNIN_REMOTE_URL at the final MCP endpoint.

  • Dependency advisory remediation (#359). Upgrade the MCP SDK to the
    patched 1.32.x family, Transformers to 4.3.x with ONNX Runtime 1.30, and the
    Vitest toolchain within 4.1.x. Refresh affected transitive packages and remove
    the vulnerable sprintf-js dependency chain. Add proxy trust-range regressions
    and Linux ARM64 native SQLite/vector/embedding smoke coverage for both models,
    fp32/q8 inference, and cache-only reloads. Embedding defaults are unchanged.

  • Production dependency advisory remediation (#356). Raise the patched
    floors for adm-zip, fast-uri, Hono, ip-address and Sharp, and pin a patched
    qs floor for OAuth/form parsing. Keep direct SDK, Transformers and ONNX Runtime
    versions stable; refresh the dated exposure/compatibility review and guard
    URI authority validation with a regression test.

Added

  • Proposal extraction evaluation foundation (#370). Add frozen synthetic
    English/Swedish cases, candidate-only labels, a deterministic in-memory
    memory_extract baseline, and common recorded-result envelopes for general
    model and HyMemory comparisons. No model or hardware run is performed;
    thresholds and labels remain pending review, and smoke output is never
    publication-eligible.

  • Bilingual retrieval evaluation foundation (refs #369). Add strict,
    hash-bound candidate/result contracts, deterministic paired scoring and
    synthetic offline smoke fixtures for all four SV/EN directions. Smoke reports
    remain incomplete and publication-ineligible; human labels, live model runs
    and target-hardware measurements remain pending.

  • Bounded code-health evidence ingestion and reconciliation export (#358).
    memory_code_health accepts frozen v1 records with principal-scoped UUID
    receipts, same-principal correction lineage, classification checks, and a
    six-calendar-month maximum retention period. Deterministic snapshots page the
    complete authorized retained set; consumers may remove absent rows only
    after a complete export. Managed evidence is excluded from generic search,
    embeddings, consolidation, and automatic derivation. Deletion clears content
    and descriptive ledger metadata while a minimal ID/hash tombstone and every
    key-bound receipt remain until their original expiry. Migration 29 binds each
    receipt alias to the principal-scoped record and fails closed when deleted
    alias lineage cannot be recovered. Migration 30 repairs the derived FTS cache,
    purging managed terms (including orphan terms from already-deleted rows) while
    preserving split-token search for ordinary entries. See docs/code-health.md.

  • Replay-safe write recovery (#321). Optional principal-scoped UUID
    idempotency_key on state writes, logs and status updates. Migration 27 stores
    metadata-only receipts atomically with write effects. Same-argument recovery
    returns the committed identifiers/status/timestamps without reapplying a write;
    changed operations conflict. Replays recheck live authorization/classification,
    survive deletion and restart, and do not reserve keys during dry runs.
    The bridge negotiates support, injects keys and reports recovery instructions
    without automatic network write retries. Stateless HTTP uses JSON responses
    so lost response bodies reach the bridge's request error path. Receipts persist
    after content deletion and cannot cover history lost by restoring an older backup.

  • Optional Heimdall status for NAS backups. When HEIMDALL_HUB_URL and
    HEIMDALL_FLEET_TOKEN are set in the ops environment, the backup publishes a
    fixed generic pass/fail status to the nas-backup panel. Delivery is best
    effort and cannot change the backup result. A systemd OnFailure alert covers
    timeouts and unexpected process kills; intentional systemctl stop does not
    trigger it. scripts/install-ops.sh installs the status helper and alert unit.
    Early validation failures leave caller-owned temporary files untouched.