Repository navigation
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
remainnot_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
releasebenchmark-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 tomain, 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) andmemory_update_status
(includingvalidate_onlypreviews) now check the existing entry against the
caller's classification ceiling, using the same predicate asmemory_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 orcreate_if_absentconflict, or see
protected status content in a preview. Such calls are now denied as hidden
targets (access_deniedfor agents,{"found": false}otherwise,
classification_errorfor 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. Theoverridesfloors are raised so the lockfile
cannot fall back.npm auditreports 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 concurrentPOST /authorizerequests 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.setLastResolvedIdentityis 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 withinvalid_scopeinstead of being issued. Expected
grant failures (unknown, replayed, expired or foreign refresh tokens and
authorization codes, redirect mismatch) now return HTTP 400invalid_grant
instead of HTTP 500server_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 publicclient_id).oauth-clients remove <client>now
also invalidates the client's approved-but-unredeemed authorization codes, and
code and refresh-token exchange fail closed withinvalid_grantwhen the client
is no longer registered.clearOAuthClientsreturns
{ mappingsRemoved, tokensRevoked, codesInvalidated }; thecleartext output
reports all three counts and its--jsonoutput addstokensRevokedand
codesInvalidated(removedstill counts mappings). Clearing a principal no
longer revokes other principals' tokens on a client it happened to own.
removeOAuthClientlikewise returns{ removed, mappingsRemoved, tokensRevoked, codesInvalidated }, andoauth-clients removeprints 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 justRSA/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 lowercasebearer <token>. All patterns require a minimum body
length so prose such as "sk_live" in docs or a shortnpm_installtag 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
BEGINline. -
Credential-shaped values in stored metadata and retrieval telemetry are now
handled like credentials incontent(#409).memory_write(including
patch.tags_add),memory_logandmemory_update_statusscan namespace, key and
tags with the same secret patterns and reject with the same
validation_errorcontract (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_queryandmemory_retrieval_feedbackmask 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_queryresponses echo the maskedqueryon every page, andmemory_read_batchfailure items mask
credential-shaped namespace/key values. Namespace and key format errors redact
credential-shaped values, andmemory_update_statusrejects a secret-bearing
namespace before any format error. Historical rows are not
rewritten; scrubbing existingretrieval_events.query_textis 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
internalbefore 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
/mcprequest 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
SIGTERMclosed 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 areunref'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_auFTS trigger to
AFTER UPDATE OF content, namespace, key, tags, rowid. Embedding-worker
writes toembedding_statusand other non-indexed columns no longer delete and
re-insert the FTS row or re-runmunin_split_tokensover 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 theOFlist. -
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 requireMUNIN_EMBEDDINGS_REVISION; missing or invalid
revisions disable optional embeddings and preserve explicit lexical fallback.
Model IDs must use Hugging Faceowner/repositorysyntax. 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ällningor
Björkroare no longer broken into unmatchable fragments. Hyphenated and
underscored compounds also contribute their parts (Jev-integrationalso
searchesjevandintegration), 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_queryno 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 carriesretrieval.candidates_truncated: trueand
retrieval.candidate_cap: 500on every page (omitted when nothing was dropped),
andtotal_matchedis then a lower bound. Thequery_bound_exceedederror 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).
addPrincipalnow 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_reviewpreview 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 topayload_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_errortelemetry. 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-readablereview_expiredcode and message
advertised by preview through the stableok:falserejection envelope.
Non-terminal approve-time rejections now run through the transactional
approval path, append one durableapproval_conflictevent per rejected
attempt, and restore the indexedexpires_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_commitmentsnow paginates the SQL-eligible open and recently-done rows
to exhaustion before filling its independent output buckets, while
memory_patternsandmemory_handoffprefilter 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_commitmentsnow recognizes future-dated verify/run forms and
explains conservative exclusions (#274). The dated-action vocabulary now
includesverify, so future phrases such asWe will verify ... by YYYY-MM-DDandI will run ... on YYYY-MM-DDsurface alongside existing
validate/check/testforms. The tool now also returns bounded,
content-blindexclusion_diagnosticswhen 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-hocNext Steps:heading. Public wording now matches the
real contract: canonical tracked-statusNext Steps, dated future clauses in
visible tracked-status prose, explicitmemory_logcommitment phrases such as
We agreed to: ...orI commit to: ..., and future-datedmemory_log
phrases can surface here. Generic non-status state fields are not commitment
sources. Legacy plain markdown status blobs with ad-hocNext 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-statusNext 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 ascompleted ... on 2026-07-31cannot be recycled into a new overdue commitment when a later
noun-subject clause saysthe 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 Itemsstay visible for dated obligation prose
even when another section contains aStatus:line. Inline canonical
**Next Steps**:lines now normalize to the same single tracked-next-step
commitment as the## Next Stepsform, 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, andmemory_logtherefore no longer emit the fake
classification_provisionalfield, and audit rows no longer stamp a
provisional classification suffix. The response, stored entry, and
memory_historynow 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 behindmemory_query, semantic exact-namespace scans,
audit history, commitments, and adjacent namespace-filtered reads previously
used SQLiteLIKE, which is ASCII case-insensitive by default and could drift
from the server's case-sensitivestartsWithchecks. Namespace subtree
filters now use exact equality plus a UTF-8-safe literal descendant range, so
supplementary Unicode descendants stay inside scope,Projects/Foono longer
matchesprojects/foo/..., trailing-slash filters remain descendant-only, and
the shared SQL helper treats wildcard-looking bytes literally instead of as
LIKEwildcards. 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
beforeLIMITonly 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_querynow
reportsnamespace_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 statevalid_fromfrom the lastupdated_at, so uncovered times could returnfound:falsewith 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 viahistory_available:falseor 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 advancevalid_fromstrictly past the prior row boundary even when two writes land in the same millisecond, so pastas_ofreads cannot return rewritten current content. Pre-creation timestamps remain ordinary misses. The exact visible currentvalid_from/updated_atboundary 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. Explicitsupersedeschains keep their half-open boundary semantics. -
memory_deletepreviews 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_countnow includes
every state revision that confirmation can delete, with
current_state_countandhistorical_state_countmaking 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 withpartial_lineagebefore it issues a token, rather than presenting
a misleading partial preview and failing only at confirmation. -
memory_deletepreview 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 withok:trueand 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_atand
content of every row in scope — content included becauseupdated_athas
only millisecond resolution, and a same-millisecond rewrite would otherwise
be invisible — and confirmation recomputes it from the same queries. A
mismatch returns the newpreview_staleerror 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,classificationandvalid_untilas well as content, because a
metadata-only patch (saytags_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
donewhile inserting the reworded successor asopen, 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/callmessages
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-markedadmission-v1429 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 randomX-Munin-Request-Idheader, reused across
retries and shown in bridge errors and network retry and failure stderr logs;
the server logs it asrequestId
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.mjsgains the cause reporting
only (identical table, tested).
Changed
-
Report bounded intake sample completeness (#367). Read-authorized
memory_writeand
memory_logreports 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_writepatches return their newupdated_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_rankernow uses
the same 500-candidate cap per retrieval leg, semantic-distance cutoff and
default expiry filtering asmemory_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_queryordering 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 tosearch_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_atvalues only, never the clock;explainfreshness_scoreremains
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_weightis 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_reviewproposals 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-leakingnot_foundresponse as an
unknown ID.scope:"principal"is an explicit same-principal cross-session
opt-in; it never expands across principals. Lists support exactnamespaceand
operation_typefilters. Legacy rows with no session remain principal-scope-only
until their normal retention lifecycle removes them. Authenticated OAuth and
agent-token HTTP requests that omitMcp-Session-Idreceive 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_querypagination 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 indexedexpires_atrange during startup and periodic maintenance for both transports, and a caller may evict only its own oldest active snapshots before the server throwscapacity_exceededrather 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 legacyaccess_fingerprintcolumn), 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 allowinglimitchanges 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, sototal_matched,returned, andhas_morestay truthful even when snapshot members disappear after page 1. Semantictotal_matchedis 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 intotal_matched. Hybridexpired_filtered_countcounts unique expired candidate IDs across both retrieval legs. -
memory_historynow exposes explicit sync bootstrap state alongside older-page navigation (#268).
Cursorless newest-first pages now returnsync_cursor, the newest visible audit id in that initial feed (or0when the feed is empty), so clients can bootstrap forward polling without guessing fromolder_cursor.older_cursorpages keep newest-first history browsing but now returnsync_cursor: null, which makes the initial watermark contract explicit. Theirhas_newerflag 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 numericcursorcompatibility and continue to advancenext_cursor, while also exposing the same value assync_cursor; empty sync pages preserve the input cursor in both fields.next_cursorremainsnullon cursorless/older pages to avoid inviting the wrong direction, and the tool metadata now documentshas_moreas a direction alias forhas_olderorhas_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_queryserialization metadata now describes reranked linear order (#252).linearpreserves the result order after structural reranking; visible lexical, semantic, and hybrid retrieval signals may differ from a result's final position.boundaryremains a display-only transform applied afterward, and retrieval analytics continue to record the linear order. -
memory_update_statusnow supports non-mutating sandbox validation viavalidate_only: true(#275).
The normal mutation path is unchanged: realmemory_update_statuswrites 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 previewedcontent/structured_status; write-only callers get
the same generic withholding notice used by the real mutation response. -
memory_orientnow has a total response budget, a beginner mode, and an
explicit response marker (#277). The per-group dashboard caps from #254
still leftstandardresponses large enough to blow past host output limits
on mature estates.memory_orientnow applies a deterministic
response_character_budgeton the final JSON payload in every detail mode,
preserving the core handshake and reporting every budget-driven omission or
truncation inresponse_budget_meta.adjustmentsinstead of failing
silently. Every orient response also includesgenerated_atso callers can
identify the specific handshake they are looking at without pretending it is
a transactional snapshot across latermemory_listcalls. 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, whilecompactremains the default first call. -
Manual
memory_consolidateis 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 eligibleprojects/*orclients/*
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_reviewpreview response shape changed (#272). The legacy
writes_memoryfield is removed. Preview now reports its own side effects
withpreview_wrote_memory:falseand 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 omitsexact_operationbecause no retained payload
remains to display.
Security
-
Safe MCP exception boundary (#368). Unexpected backend failures return a
fixedinternal_errormessage 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.mjsfrom 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 pointMUNIN_REMOTE_URLat 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_extractbaseline, 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_healthaccepts 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. Seedocs/code-health.md. -
Replay-safe write recovery (#321). Optional principal-scoped UUID
idempotency_keyon 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_URLand
HEIMDALL_FLEET_TOKENare set in the ops environment, the backup publishes a
fixed generic pass/fail status to thenas-backuppanel. Delivery is best
effort and cannot change the backup result. A systemdOnFailurealert covers
timeouts and unexpected process kills; intentionalsystemctl stopdoes not
trigger it.scripts/install-ops.shinstalls the status helper and alert unit.
Early validation failures leave caller-owned temporary files untouched.