v0.1.41
Upgrade notes
- The response cache is upgraded to schema v4 and performs a one-time cache
reset on first run after upgrade; a cache-hit now retains JSON-safe
structuredContentand result_meta.mms daemon statusand the daemon
ping gain queue-telemetry fields in both human and JSON output. Opt-in
Unicode token estimation and_context_queryschema advertisement are both
off by default (character-budget behavior unchanged). (#731) - On a core reporting a non-RRF
score_scale, surfacing now injects up to
max_resultsmemories per call where it previously filtered most or all of
them out against an incommensurablemin_scorethreshold; dedup and feedback
demotion remain the quality controls. Set
surfacing.scale_gated_min_score=falseto restore unconditional filtering.
(#730) - On a bypass-capable core with server-side rerank enabled, surfacing scores
now return on the RRF scale ((0, ~0.033]) thatmin_scoreand the
auto-tuner are calibrated against; setsurfacing.rerank=noneto restore
server rerank policy. (#726) - With a compatible Toolgraph and the default
on_unreachable: open, a typed
backend outage now starts loudly degraded under STM-native rules instead of
failing startup as a protocol error. (#736) hook.daemon_timeout_seconds=inf/nanis now a configValidationError
instead of an accepted value. (#722)- Default daemon installs now write
surfacing_eventstelemetry rows on the
hook path; growth is bounded by the existingstats_retention_days/
query_retention_dayscleanup. (#723)
Added
-
Developer-first project routing and runtime controls for the proxy.
mms project routepreviews and applies a project-selected registry entry with
config locking, validation, backups, provenance, conflict protection, and
idempotency (previews by default; never overwrites a name/prefix conflict or
prunes routes). Opt-in Unicode runtime token estimation
(token_estimation_mode, defaultstatic— character-budget behavior
unchanged) resolves with proxy/server/tool precedence.mms statusandmms doctorsurface compression-tuning readiness._context_queryschema
advertisement is available opt-in (disabled by default) and never forwards
the proxy-only argument upstream. Behavior change: the response cache is
upgraded to schema v4 so a cache-hit now retains JSON-safe
structuredContentand result_meta(errors and mixed/non-text responses
remain uncached); the upgrade performs a one-time cache reset.mms daemon statusand the daemon ping now expose bounded queue telemetry, adding fields
to both the human and JSON output. (#731) -
Surfacing now reads the score scale a compose-capable core names on the
composed bundle itself (score_scale/rerankeron thecontext_compose
envelope,context_composeschema 4, core #1796, first available in the
core release after v0.3.11). This closes the compose blind spot the
mem_searchscore-scale work (#727) left open: STM stamps the scale onto
the compose retrieved results (never pinned blocks), so the scale gate
(#730), the definitivescore_scale_mismatchdiagnostic (#727), the
relevance-bucket suppression,surfacing_eventspersistence, and
stm_surfacing_statsall now cover the composed retrieval path, not just
the legacymem_searchfallback. The two keys cross the hook-daemon wire
additively (noPROTOCOL_VERSIONbump, emitted only at the negotiated
schema 4) and survive the surfacing cache. Dormant against every released
core (schema ≤ 3 never carries the keys → all-None→ today's behavior);
the live effect requires a core release carrying #1796, a reinstall, and a
daemon restart. The process-levelstm_surfacing_statsScore scale:
line now also reflects compose batches. (#734) -
Surfacing now suspends the RRF-calibrated
min_scorefilter when the core
names a foreign score scale.min_score(default 0.03) and the auto-tuner
are calibrated against RRF fusion scores ((0, ~0.033]); on a batch whose
core-reportedscore_scale(#727) is a known non-RRF label
(bm25/dense/none/rerank— e.g. raw cross-encoder logits with a
negative median), no fixed constant is meaningful, so the global/auto-tuned
floor is not applied (results stay bounded bymax_results) and auto-tune
pauses:maybe_adjustskips the batch and the tuner's rating ratios count
only feedback earned on RRF-stamped or unstamped surfacings. Per-tool
context_tools.<name>.min_scorepins always keep the filter active; the
relevance-bucket tags ([weak]/[related]/[strong]) are suppressed for
results stamped with a non-RRF scale (the[min_score, 1.0]band math only
holds on RRF); the first suspended batch marks a lingering
score_scale_mismatch/score_ceiling_below_minepisode recovered, so the
score_scale_mismatchdiagnostic andmms doctor'sltm_score_scaleFAIL
now occur exactly when the filter actually applies (pin present, or gate
disabled). Newsurfacing.scale_gated_min_scorebool
(MEMTOMEM_STM_SURFACING__SCALE_GATED_MIN_SCORE, defaulttrue); set
falseto restore unconditional filtering. Unstamped batches (compact
format, pre-#1781 cores, and compose bundles on a core older than the
schema-4 stamp) and unrecognized labels behave exactly as before; #734 later
in this release extends the scale stamp to the composed retrieval path on a
schema-4 core, so the gate is no longer limited to the legacymem_search
fallback.
Behavior change: on a core reporting a non-RRF scale, surfacing now
typically injects up tomax_resultsmemories per call where it previously
filtered most or all of them out on an incommensurable threshold — dedup
and feedback demotion remain the quality controls. (#730) -
Surfacing now reads the score scale the core names in structured
mem_searchoutput (score_scale:rrf/bm25/dense/none/rerank
plus the activererankermodel ID, core #1781, first available in the
core release after v0.3.11) and threads it through observability:
every parsed result carries the scale (including across the hook-daemon
wire and the surfacing cache), eachsurfacing_eventsrow records it
(additive nullable column, migrated in place),stm_surfacing_stats
renders aScore scale:line plus a per-scale event distribution, and
the below-threshold tripwire gains a definitive tier — when the core
names a non-RRF scale while the ceiling sits under the RRF-calibrated
min_score, STM firesscore_scale_mismatchon first observation
(no five-call streak) andmms doctornames the mismatch and the fix.
Observability only: filtering, thresholds, and unreported-scale paths
(compactformat, older cores, and — until #734 later in this release —
compose) behave exactly as before. (#727) -
Surfacing retrievals now ask the core to skip its cross-encoder rerank
stage per call (core #1766, first available in the core release after
v0.3.11). On a rerank-enabled core that stage is ~99% of retrieval latency
(compose p50 4.2s vs 42ms bypassed) and blew the surfacing budget on every
builtin call, while survival past the defaultmin_scoreis measured
identical either way — the bypass trades ranking precision, not result
existence. Newsurfacing.reranktri-state (MEMTOMEM_STM_SURFACING__RERANK):
false(default) bypasses,trueforces the server-configured rerank,
noneomits the parameter so server config decides. The parameter is only
sent when the connected core advertises it in itsmem_searchtool schema
(negotiated once per session, like theresult_formatdowngrade), so on
older cores every value degrades to today's behavior instead of tripping
the server's argument validation and charging the circuit breaker.
Behavior change: on a bypass-capable core with rerank enabled
server-side, surfacing scores return on the RRF scale ((0, ~0.033]) —
the scalemin_scoreand the auto-tuner were calibrated against; set
rerank: noneto restore server policy. (#726)
Fixed
- Aligned proxy CLI defaults and clarified
mms ejectrecovery. The default
STM proxy config path is now centralized across CLI modules, andmms project route's runtime resolution stays consistent with its help output. An
origin-lessmms ejectfailure now lists the valid--totargets, and
prune-backup hints render as a concrete retry command. (#732) - Toolgraph stdio consults now recognize the producer's typed
backend_unavailableMCP envelope and route it throughon_unreachable;
legacy, unknown, malformed, and contract errors remain on
on_protocol_error. Behavior change: with compatible Toolgraph and the
defaulton_unreachable: open, a backend outage now starts loudly degraded
under STM-native rules instead of failing startup as a protocol error. (#736) - A transient DB failure while closing a
score_scale_mismatchdiagnostic no
longer leaves the persisted episode open until the next full mismatch cycle.
The warning latch still re-arms immediately on a healthy observation, while
the recovery UPDATE now retries on later healthy observations or
scale-gated suspended batches until it succeeds. (#733) - The daemon now hands the surfacing engine the time left in the client's
deadline instead of letting that deadline cancel the engine from outside.
hook.daemon_timeout_seconds(2.5s) is smaller than
surfacing.timeout_seconds(3.0s), so on the daemon path every slow LTM
search was aborted by the transport before the engine's own timeout could
fire. That abort raisedCancelledError, which bypasses the engine's
asyncio.TimeoutErrorhandler — the one place that records the
error_timeoutfault, logs the warning, and counts the failure toward the
circuit breaker (#579). The result was an invisible failure loop: no fault
rows, no log line, and a breaker that could never open, so every eligible
hook call re-paid the full timeout and respawned the LTM stdio child. A slow
LTM now trips the breaker as designed and shows up in
mms stats/surfacing_faults. When queue and lock wait have already eaten
the deadline, the daemon skips the LTM round trip entirely rather than
starting one it must cancel mid-RPC. A surfacing call that times out inside
the engine is also recorded as atimeoutlatency sample rather than a
successone roughly the length of the whole budget, keeping the censored
duration out of the percentilesmms daemon status's timeout recommendation
is derived from. (#719) - Closed the race #719 left between the surfacing engine's own timeout and the
daemon's outer deadline backstop. The engine now receives the client's
deadline as an absolute monotonic point rather than a relative budget, and
derives its window right before the LTM attempt — so its pre-timeout work
(gate, query extraction, privacy scan) debits that window instead of silently
eating the fixed response margin. The remaining gap was that the engine only
raised its timeout once the LTM adapter it had just cancelled finished
unwinding — an unbounded wait, since a stdio child can be slow to give up —
which is what let the caller's backstop cancel the engine from outside first
and skip theerror_timeoutfault row, the warning log, and the
circuit-breaker increment that only the engine's own timeout path records
(#579). The LTM operation is now shielded, so that abort lands the moment the
engine's timer fires and the abandoned unwind is left to finish on its own
(the adapter already expects a caller to leave mid-RPC and marks the session
for lazy reconnect; shutdown waits a bounded moment for that cleanup rather
than cancelling it a second time, and declines new attempts once too many
cancelled operations are still unwinding — warning once per draining episode
and refunding the rate-limit slot it claimed, since a refusal starts no LTM
work). The engine also
books its timeout off which timer fired — a flag set inside the timer
callback, which the loop runs in scheduled order ahead of any backstop
scheduled later — rather than off elapsed time, the caller's deadline, or a
timeout scope's own expiry, none of which can tell "my timer fired first"
from "my timer also fired, later, while something else was cancelling me".
A cancellation that is not this call's own timeout is left unbooked, so a
shutdown or a client hanging up never charges a healthy LTM a breaker
failure. A window fully consumed by pre-work is booked without starting an
LTM round trip that would be cancelled mid-RPC, and gives its rate-limit
slot back the same way a refusal does. (#721) - The daemon's admission check now rejects a
deadline_monotonicit cannot
actually enforce, answeringexpiredinstead of admitting the request:
NaNcomparedFalseagainst the expiry check and reached
asyncio.timeout_at(NaN),+infadmitted with a backstop that can never
fire, and an int too large for a float made the dispatch raise instead of
respond. #721 fixed the same family in_surface_deadline; both sites now
share one validation helper. The one configuration that could legitimately
produce an infinite deadline is closed at the source:
hook.daemon_timeout_secondsmust now be finite (it becomes the client
deadline asnow + budget, so+infis not a big budget but a deadline
the daemon can never enforce — and would have every request rejected).
Behavior change:daemon_timeout_seconds=inf/nanis a config
ValidationErrorinstead of an accepted value. (#722) - Successful surfacing on the hook/daemon path now records a
surfacing_events
telemetry row, sostm_surfacing_stats/mms stats/mms doctorno
longer report 0 events for a working hook path ("0 events" was previously
indistinguishable from "0 surfacing" — onlyseen_memoriesand
surfacing_faultswere written there). Hook-path rows carry
server='builtin'and asha256:digest in place of the query (the daemon
forcespersist_query_text=false), and the injected block still advertises
nostm_surfacing_feedbackrating prompt.hook.record_feedback_eventsnow
gates only the rating prompt and feedback loop, not event persistence.
Behavior change: default daemon installs start writing these rows;
growth is bounded by the existingstats_retention_days/
query_retention_dayscleanup. (#723)
Security
- Raised the declared
mcp[cli]floor to>=1.28.1so a fresh install cannot
resolve a version affected by CVE-2026-52869 and CVE-2026-52870 (fixed in
1.27.2) or CVE-2026-59950 (fixed in 1.28.1). #728 moved the lockfile to
1.28.1 to clear thepip-auditCI gate, but the published wheel resolves
against the declared floor, notuv.lock— the previous>=1.26.0still let
a downstream install pull a vulnerablemcp. The floor now matches the
audited lock. (#737)