Upgrade notes
- Behavior change: STM can now export its own spans over OTLP/HTTP to an
OpenTelemetry collector (MEMTOMEM_STM_OTLP__ENABLED=1). This is a new
outbound network path and is off by default; enabling it requires the
newotlpextra. Exported attributes are body-free by construction — no
response content, no error messages, no tool arguments. See
OTLP Span Export. (#791, #792) - Behavior change: root STM startup-configuration validation errors no
longer render pydantic'sinput_value. A block that failed to coerce as a
whole previously echoed it verbatim in the error the server logs and
surfaces to MCP clients, which could includeotlp.headersor
langfuse.secret_key. Two limits worth knowing: it covers errors raised
throughSTMConfig(validating a sub-model directly still renders
input_value), and it covers that field only — a validator that
deliberately interpolates a value into its own message (e.g.daemon.host)
still shows it. (#792) - Behavior change: Toolgraph identity-bearing request and verdict fields
that contain a lone surrogate are now refused as protocol errors under
on_protocol_error; they are never non-injectively escaped. That knob
defaults tofail_start, so aneligible_toolspayload carrying one
refuses server startup rather than degrading — and the response check is
deliberately whole-payload, covering identity fields STM does not itself
read (eligible,tool_key). Set the knob toopenorclosedif you
would rather degrade than block on a provider you do not control. Request
validation runs before the consult cache branch, so cold and warm starts have
the same enforcement posture; because a cached row stores only the verdict's
raw facts, response-field identities cannot be revalidated from it, so rows
written before this policy are dropped and one full consult re-runs after
upgrade. Stored SQLite identifiers in compression feedback, metrics,
progressive-read telemetry, and surfacing feedback are likewise refused
rather than rewritten. (#788, fixes #783) - The response cache is upgraded to schema v5 and performs a one-time cache
reset on first run after upgrade: the key derivation is now framed, so every
stored key changes and the pre-upgrade rows are unreachable. No
configuration or API change; entries repopulate on use. (#795, fixes #784) - Behavior change:
mms stats --toolnow refuses a filter that is not
valid UTF-8 as a usage error (exit 2) instead of reporting an all-time zero
(--json) or dying withUnicodeEncodeErroron the filter echo (human
form). (#788, fixes #783)
Added
- docs: record the ecosystem integration decision as ADR 0001 — per-boundary
contracts with named deferral gates (tracegraph, agent-guard, vigil,
syncmill) instead of a generic adapter, plus adocs/adr/index and drift
pins for the paths and claims the ADR makes. Stage ordering/status lives in
the tracking issue (#789). (#790) - observability: opt-in OTLP/HTTP span export, satisfying ADR 0001's
otlp-telemetry-exportgate. Spans carry real W3C trace/span ids and real
in-process parentage —proxy_callwith the pipeline stages and a new
upstream_rpcspan nested under it — rather than anything reconstructed
from the selection log. Attributes are admitted per span and only when STM
itself derived the value, so response content, error messages, tool
arguments and configured header values never become telemetry. (Headers are
still sent as HTTP headers on the export request — that is what
authenticates STM to the collector.)
Enable with theotlpextra andMEMTOMEM_STM_OTLP__*; counters surface
instm_proxy_health. (#791, #792)
Fixed
-
proxy: derive the tool-graph consult cache's scope key from framed
components, closing the same collision #784 fixed in the response cache. The
old derivation joined five components on a bare NUL, andagent_id/
query_profileare free-form and adjacent — nothing on the path rejects a
NUL in either, sincevalidate_toolgraph_identifierrefuses lone surrogates
only — so two distinct scopes could share one row and a consult's cached
facts could decide policy for a different scope. The framing primitive is now
shared by both caches (utils/digest.framed_digest) rather than written
twice; the response cache's derivation is byte-identical and its rows are not
affected. The consult cache gains a privatetoolgraph_metatable stamping
its scope-key version, so the rows orphaned by this change are purged once on
first start instead of occupying scope slots until_trimages them out —
deliberately notPRAGMA user_version, which belongs to the database rather
than the table and so would skip the purge wheneverconsult_cache_path
points at a file another component already stamped. (#797, fixes #794) -
cli: display-escape the remaining raw terminal renders in
mms host,mms projectand the shared write lock — the timestamp fieldslast_imported
andlast_seen, the registry/sidecar paths in the fivemms host sync --applysummary lines, themms project list --prunelock-timeout and
mms project routeconfig-error messages, and all four write-lock
boundaries. The timestamps are typed as barestr, so a hand-edited or
imported TOML can put a CR or ESC in one (a lone surrogate it cannot — TOML
refuses a non-scalar escape); the paths areHOME-derived, where an
undecodable byte becomes a lone surrogate and the render raises after the
write has already landed.--jsonoutput is unchanged: the escape is at the
human render, and the config-lock JSON envelope still carries the logical
raw message. (#796, fixes #786) -
proxy: derive the response-cache key from framed components so it is
injective over the serialized
(server, tool, args, context_query, config_fingerprint)—argsby its
JSON rendering, which is what the upstream tool receives, so trees that
render identically keep sharing one row on purpose.
The old derivation joined the components on a bare NUL and serialized two of
them withensure_ascii=True, so two distinct calls could hash to the same
key and one call's cached body was served for the other: a NUL inside an
upstream server or tool name shifted the component boundary (nothing on the
path rejects one), and an astral scalar in the arguments rendered as the
same escaped text as two lone surrogate code units. Each component is now
length-prefixed and serialized withensure_ascii=False, behind a
_KEY_SCHEMA_VERSIONbump to v5. (#795, fixes #784) -
observability: refuse an OTLP endpoint whose path trips the credential
screen, and narrow the SDK log screen. The HTTP transport logs its request
line on every successful export, so a token in the endpoint path was
written out in full by a logger STM does not own — refusing the endpoint is
the boundary STM controls, andurllib3/requestsare deliberately left
unfiltered since the whole process shares them. The screen on the two
OpenTelemetry loggers now inspects every formatter-visible channel
(exc_info,stack_info,extra=, not just the message), drops rather
than rewrites a record that trips it, fails closed on a record it cannot
render, and is detached at shutdown. (#793) -
config: keep a configured credential out of the startup validation error the
server logs and surfaces to MCP clients. A child model's
hide_input_in_errorsdoes not govern an error raised on the parent field,
so a block that failed to coerce as a whole —otlpcarrying itsheaders,
langfusecarrying itssecret_key— rendered verbatim as pydantic's
input_value. The setting is now onSTMConfigitself, which is the path
every startup validation takes;OtlpExportConfigsets it too, so validating
that one model directly is covered as well. The other sub-models are not, and
a validator that interpolates a value into its own message (e.g.
DaemonConfig.host) still shows it either way. (#792) -
Lone surrogates in SQLite-bound diagnostic/content fields and nested
extraction JSON are escaped once at ingest, while legacy surfacing memory-ID
JSON omits unencodable identifiers on display/stat reads without aliasing
them to an existing literal\udxxxID. Query-only digest inputs in
surfacing cache keys, persisted query hashes, and tool-relevance telemetry
now hash withsurrogatepass, preserving clean digest values and keeping a
raw surrogate distinct from its six-character literal twin. Observability
stats filters and surfacing-feedback identifiers are rejected at their
response boundary with sanitized, UTF-8-safe errors. (#788, fixes #783) -
Negative-feedback counts now drop only the unencodable memory IDs instead of
the whole batch, so one bad ID no longer suppresses surfacing demotion for
the valid IDs beside it. A refused stats filter also keeps reporting the
schema-capability flags (schema_outdated,
diagnostics_recovery_supported) accurately: they describe the file, not the
filter. (#788, fixes #783) -
Core memory IDs are no longer aliased at ingest.
_core_json_loadsscrubbed
the whole parsed payload, so a chunk ID core sent as a real lone surrogate
and one sent as the six literal characters\ud800arrived as the same
value — and the new identifier refusal, running after that rewrite, accepted
it. Content is still escaped at ingest, butid/chunk_id/block_id
now arrive unmodified and the search and context-compose parsers drop an item
whose ID they cannot encode. (#788, fixes #783)