Skip to content

v0.1.44

Latest

Choose a tag to compare

@memtomem memtomem released this 30 Jul 02:57
b6a2dc9

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
    new otlp extra. 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's input_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 include otlp.headers or
    langfuse.secret_key. Two limits worth knowing: it covers errors raised
    through STMConfig (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 to fail_start, so an eligible_tools payload 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 to open or closed if 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 --tool now 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 with UnicodeEncodeError on 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 a docs/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-export gate. Spans carry real W3C trace/span ids and real
    in-process parentage — proxy_call with the pipeline stages and a new
    upstream_rpc span 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 the otlp extra and MEMTOMEM_STM_OTLP__*; counters surface
    in stm_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, and agent_id /
    query_profile are free-form and adjacent — nothing on the path rejects a
    NUL in either, since validate_toolgraph_identifier refuses 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 private toolgraph_meta table stamping
    its scope-key version, so the rows orphaned by this change are purged once on
    first start instead of occupying scope slots until _trim ages them out —
    deliberately not PRAGMA user_version, which belongs to the database rather
    than the table and so would skip the purge whenever consult_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 project and the shared write lock — the timestamp fields last_imported
    and last_seen, the registry/sidecar paths in the five mms host sync --apply summary lines, the mms project list --prune lock-timeout and
    mms project route config-error messages, and all four write-lock
    boundaries. The timestamps are typed as bare str, 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 are HOME-derived, where an
    undecodable byte becomes a lone surrogate and the render raises after the
    write has already landed. --json output 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)args by 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 with ensure_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 with ensure_ascii=False, behind a
    _KEY_SCHEMA_VERSION bump 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, and urllib3/requests are 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_errors does not govern an error raised on the parent field,
    so a block that failed to coerce as a whole — otlp carrying its headers,
    langfuse carrying its secret_key — rendered verbatim as pydantic's
    input_value. The setting is now on STMConfig itself, which is the path
    every startup validation takes; OtlpExportConfig sets 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 \udxxx ID. Query-only digest inputs in
    surfacing cache keys, persisted query hashes, and tool-relevance telemetry
    now hash with surrogatepass, 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_loads scrubbed
    the whole parsed payload, so a chunk ID core sent as a real lone surrogate
    and one sent as the six literal characters \ud800 arrived as the same
    value — and the new identifier refusal, running after that rewrite, accepted
    it. Content is still escaped at ingest, but id / 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)