Releases: goldenwo/universal-memory
Release list
v1.22.0 — Codex CLI session capture + hooks tolerate the per-IP rate limit
- Codex CLI sessions are now captured (PR #305): the Stop hook parses Codex rollout JSONL (
response_item/messageuser/assistant records) through the same cursor / per-fire cap / dedup path as Claude transcripts, skipping Codex-injected first-turn blocks (<recommended_plugins>,# AGENTS.md instructions for …,<environment_context>) and everything that is not conversation (developer-role hook context, reasoning, function calls, event duplicates). Previously every Codex fire was a silentskip=empty-delta-filtered. Fixture + S29/S30 pin the wire bodies; verified end-to-end on a real rollout. - Hooks tolerate the server's per-IP rate limit (PR #307): every hook API call retries a 429 once after one second (
UM_API_RETRY_429,UM_API_RETRY_429_SLEEP); session-start caches a healthy write probe for 10 minutes (UM_PROBE_CACHE_MIN,~/.um/state/probe-ok) so a burst of session restores makes one fewer call per session; session-end logs a 429 aserror=http-429instead ofserver-too-old. Root cause: eight sessions restored inside one minute ≈ 40 calls into a burst of 10 (60 rpm / burst 10 per IP). - Test hygiene (PR #306):
reaction-gate-eval.test.mjspins the 400-day ledger retention its frozen frame presupposes — its 2026-08-10 fixtures had crossed the 30-day default prune, turning CI red on unchanged code.
Deploy notes: hooks-only release — no server behaviour change (the image is rebuilt for version coherence; install.sh --upgrade and the CLI refresh keep the three surfaces in step). Consumer machines: claude plugin marketplace update universal-memory + reinstall; a Codex host must also refresh its cached plugin copy. Post-deploy proof-of-life: a codex exec run logs stop posted http=200 n=<k> in ~/.um/hook.log for its project instead of skip=empty-delta-filtered; two session starts within 10 minutes log probe cached writes=enabled on the second.
Artifact verification: ghcr.io/goldenwo/universal-memory-server:1.22.0 built and pushed for amd64 + arm64 by release.yml run 34483723016 (both legs green). No server code changed since v1.21.1 (verified amd64 + arm64 there); this image differs only in its version fields. Hooks: every hooks/*.test.sh + hooks/lib/*.test.sh suite green on Windows git-bash and ubuntu CI; the Codex capture path verified end-to-end on a real rollout. arm64: deployed to the Pi (aarch64) by install.sh --upgrade 1.22.0 — pre-flight passed, container swapped, /health 200 with all 610 memories intact, CLI 1.22.0, rollback kept as um-rollback:previous. Post-deploy proof-of-life on the shipped hooks: two codex exec runs logged stop posted http=200 n=2 project=claude-harness-toolkit (first) and probe cached writes=enabled (second); initialize over MCP reports 1.22.0 and a notification still gets 202.
v1.21.1 — MCP notifications acknowledged with 202 (Codex CLI rmcp connect fix)
- MCP transport: notifications acknowledged with 202 (PR #302):
POST /mcpnow answers a JSON-RPC notification (any message without anid, e.g.notifications/initialized) with HTTP 202 Accepted and no body, per the MCP streamable-HTTP transport — previously 200 +Content-Type: application/json+ an empty body, which is not a JSON document. Fixes OpenAI Codex CLI (rmcp client) dropping theumserver at connect time (worker quit with fatal: Deserialize error: EOF while parsing a value at line 1 column 0, when send initialized notification, retried 3× → no memory tools in Codex). Responses to requests (messages carrying anid) are byte-identical. The OpenAPI/mcppath documents the 202.
Deploy notes: server-only change; no env, config, schema, or data-shape change; no CLI or plugin behaviour change (their version fields move in lockstep as always). Post-deploy proof-of-life: POST /mcp with {"jsonrpc":"2.0","method":"notifications/initialized"} returns 202 with no Content-Type and an empty body; initialize still returns 200 JSON with serverInfo.version = 1.21.1; codex exec … "Reply OK" logs no rmcp EOF error and codex mcp list shows um enabled.
Artifact verification: ghcr.io/goldenwo/universal-memory-server:1.21.1 manifest carries amd64 + arm64 (release.yml run 34152798727, both legs green). amd64 pulled and booted locally against a throwaway qdrant v1.13.0 with UM_TEST_MOCK_SDK=1 (node v22.23.2 in-image): /health 200; POST /mcp notifications/initialized and notifications/cancelled → 202 Accepted, no Content-Type, 0 bytes; initialize → 200 application/json with serverInfo.version 1.21.1; tools/list → 4 tools; unknown method with an id → JSON-RPC -32601. arm64: verified on the Pi (aarch64) by install.sh --upgrade 1.21.1 — pre-flight passed (image loads mem0ai/oss + lib/stats.mjs), container swapped, /health 200 with all 608 memories intact; rollback image kept as um-rollback:previous.
v1.21.0 — relative undated imputation (inert) + IMPUTATION-STUCK alert + deps
- #297 relative undated imputation (PR #298): the undated decay factor is no longer a constant — it tracks the dated cohort's live age quantile (median; type-7; future-dated points beyond the skew tolerance excluded; min-cohort 20), refreshed hourly from a full-corpus scan by an attempt-keyed cache with a 60 s scan bound, and derived per request at the configured half-life. Falls back to
exp(-0.25)until a statistic exists. Inert: nothing changes whileUM_TEMPORAL_DECAYis off (the default). New/api/stats.undated_imputationblock (19 snake_case keys;applied_factoris 1 andfactornull with decay off) and one row on/control. Keyed evaluation and the flip-time inputs are recorded on #239. - IMPUTATION-STUCK alert section (PR #300):
um-alert.shgains a sixth section over that block — the two spec §4.5 stuck-cache conditions (last refresh failed; last attempt more than 2 × TTL after the last success). Quiet while decay is off; a pre-#297 server prints a one-line ABSENT breadcrumb. - Dependencies (PR #299): uuid 9.0.1 → 14.0.2, @anthropic-ai/sdk 0.117.1 → 0.122.0; lock regenerated in a Node 22 container (supersedes dependabot #280/#287).
Deploy notes: the server changes are inert until the #239 flip. The alert section lives in the CLI, so the Pi cron only gains it after installer/install-cli.sh re-runs (part of the deploy runbook); consumer machines refresh the plugin as usual (claude plugin marketplace update universal-memory + reinstall). Post-deploy proof-of-life with decay off: /api/stats.undated_imputation reads enabled:false, mode:"fallback", factor:null, applied_factor:1, computed_age_ms:null, and docker logs --since 24h um-server | grep -c 'undated-imputation' is 0.
Artifact verification: ghcr.io/goldenwo/universal-memory-server:1.21.0 pulled and booted per-arch (amd64 + arm64 under QEMU) against a throwaway qdrant with the real provider key — /health 200 on both, mem0ai/oss resolves on both (node v22.23.2), uuid 14.0.2 in-image, /api/stats.undated_imputation reads enabled:false / mode:fallback / factor:null / applied_factor:1 on both, zero undated-imputation log lines at boot.
v1.20.0 — root-named project slugs + ADR relations at registration
- #294 root-named project slugs (PR #295): Project slugs are now named after the project ROOT (
.gitdominant,UM_PROJECT_MARKERSrespected). Existing subdir-named layers are NOT migrated and age out. The only operator action is the normal plugin/install-cli refresh.um-previewfrom a subdirectory now needs--project. Success hook.log lines carryproject=<slug>. Recall reads the same slug capture writes (session-start adopts the guard). Live-verified post-merge from fresh sessions in a repo subdirectory and in a.git-less marker project: session-start and session-end both logged the root slug; no subdir-named layer minted. - #271 ADR relations at registration (PR #293):
/adr syncnow carriessupersedes/superseded_byfrom the ADR's frontmatter into the registered point (label-only — status still derives from the file's ownstatus:; omitted when empty; follows the file on every re-sync). - Zero server runtime changes; the image is rebuilt for the version stamp only.
Deploy notes: the slug change lives in the hooks and CLI, not the server — every consumer machine keeps the old cwd-basename derivation until it refreshes the plugin (claude plugin marketplace update universal-memory + reinstall) and the installed CLI (installer/install-cli.sh). Until then, old-derivation junk layers re-firing LAYERS-STALE is the alert working. The install-cli refresh also clears the #271 installed-copy residual (create-adr lib).
Artifact verification: ghcr.io/goldenwo/universal-memory-server:1.20.0 pulled and booted per-arch (amd64 + arm64 under QEMU) against a throwaway qdrant with the real provider key — /health 200 on both, mem0ai/oss resolves on both (node v22.23.2).
v1.19.0 — crash-dead capture alarm + ADR status derivation
- #283 crash-dead capture alarm (PR #291):
/api/statscapture entries gaincheckpoint_abstained_7d+last_turn_day; um-alert gains the fifth section CRASH-DEAD — pages when a surface that used to post turns goes silent while session-end keeps stamping abstained checkpoints (the one silent-death class no deployed instrument could see). Post-deploy live-fire verified on the Pi. - #275 ADR status derivation (PR #290):
adr_statusnow derives the server-managed point status on the identity path — retired ADRs leave default recall (include_superseded=truerecovers them); un-suppression = flip frontmatter + re-sync. Live-verified: ADR-0004 gone from default search post-sync. - #285 (PR #288):
create-adr.shgains the~/.um/auth-tokenfile tier — bare/adr syncauths on hooks-configured machines. - #286: append-turn field-validates before the F1 soft-default — no more warn-then-400 log pairs from the healthy G7 probe.
- Test hygiene: #289 (reaction-ledger fixture clock un-pinned).
Deploy notes: refresh the installed CLI (install-cli.sh) with the server swap — the um-alert cron copy needs it for the new section (the #283 checklist's step 0 verifies).
v1.18.0 — capture-anomaly alarm + identity-addressed ADR writes
v1.18.0 — the capture-anomaly alarm + identity-addressed ADR writes
- stop.sh now self-reports its six measured-zero silent-death reasons as
signal.capture_anomalycounter rows via POST/api/capture-anomaly(pure bash+curl helperum_signal_anomaly— a no-python client still reports; 429 carved ashttp-429ahead of theserver-too-oldresidual). skip=empty-deltasplit into-stalled/-filtered(exhaustive shape classification; pass-2 always emits EMPTY)./api/statsexposes an independentanomaliessection (fail-isolated toanomalies:null; always-presentsignalskey: absent ⇔ old server / null ⇔ degraded /{}⇔ healthy).- um-alert SIGNALS escalation (ABSENT/DEGRADED/ERROR/ALERT/OK; ALERT = any surface
count_7d>0) with print-all on every exit path — also fixed the pre-existing LAYERS-masks-LEDGER and the STALE arm's silent LEDGER drop. - Version skew is safe both orders: a new hook against an old server logs
signal=server-too-oldquietly. - Residual (crash-dead class) deliberately unalarmed pending its measured base rate — #283.
Identity-addressed ADR writes (#279, PR #281):
type:'adr'writes with a usableadr_idget a deterministic point id from(repo_path, adr_id, userId)— idempotent full-replace, dedup bypassed, verbatim title,createdAtpreserved, suppression-state carry-forward. A re-synced ADR no longer mints a duplicate point.
Also riding:
- ADR graph derived from decision files (#266, PR #270) + a corrected header rationale (#278).
- Memory link audit CLI (
cli/memory-audit.mjs, #273). - Safe half of the #258 dependency batch (#274); the three measured-bad majors stay ignored.
Registry verification (2026-08-24, pre-announce): both published arches pulled and booted from GHCR against a scratch qdrant — amd64 and arm64 (QEMU): 1 patch warn each, /health 200, in-image v22.23.2. Deployed same day: /health 200 with the full corpus intact.
Post-deploy checklist: #279 legacy cleanup (list-first, ≤8 expected; ungates #275) → #267 §14 ritual (mount probe → deliberate fire → D4 read → CLEANUP) → #283 measurement SQL → alarm-or-park decision.
v1.17.0 — safety fixes + Node 22 base image + mem0ai 3.x riders
Safety / correctness (the reason this release should not wait):
- memory_forget / reindex no longer resurrects or duplicates — three stacked fixes: vault-authored lifecycle status survives the trusted reindex path (#261);
state.mdvalid_fromis server-stamped, never model-invented (#265 — a live vault showed 25/27 state docs with fabricated 2023 dates and one FUTURE date); the delete enumeration fails LOUD on a saturated scan window instead of silently deleting off a truncated view (#268; the live 100-point enumeration window itself was fixed by #231's FULL_SCAN_LIMIT). - Summarizer fabrication (#259) was fixed by the v1.16.0 abstention floors; detection note for pre-fix corpora:
tokens_in == 182alone is the marker (NOT missingcovers_from).
Base image + dependency majors (#255):
- node:22-alpine both stages (Node 20 EOL), engines >=22, six CI Node pins (two jobs were silently floating on runner defaults).
- openai ^7.5.0 (engines-only major). better-sqlite3 stays ^12.9.0 — 13.x measured unshippable:
npm cifires the implicit node-gyp rule on its toolchain-less binding.gyp (repro on #255); its dependabot ignored-major stays. - Docker post-prune gate now LOADS the better-sqlite3 native binding on both arches (fail-loud with the gate's own diagnostic); NEW standing nightly arm64 build job gives the gate pre-tag signal.
mem0ai 3.x reconciliation riders (#231):
- Boot log: 12
[mem0-patch]warns → 1 (pg only). - dependabot now ignores mem0ai (PRs only; Security-tab alerts remain the advisory path). patches/README.md is the durable reconciliation reference.
- @qdrant/js-client-rest pinned 1.18.0 (1.19 removed the legacy search API).
Registry verification (2026-08-19, pre-announce): both published arches pulled and booted from GHCR — amd64 and arm64 (QEMU): 1 patch warn, /health 200, in-image arm64 v22.23.2.
Post-deploy checklist: #262 one-time duplicate cleanup (targeted qdrant deletes, payload key id; NO vault reindex — #215 frame) → status-breakdown re-check (pre-deploy: 454/454 current) → close #262.
v1.16.0 — cursor-chunked checkpoint + um-drain
Session-end checkpoints read the raw capture backlog cumulatively from zero on every fire, with a single unbounded summarization call. Once the backlog outgrew one model call (~4MB after a month of quiet failure), every checkpoint attempt hit the same cliff, failed, and — because the failure path emitted no counter — reported nothing. Capture looked alive; digestion had been dead since early August. Issue #247 carries the full investigation.
This release makes checkpoint input bounded by construction, and makes the stats surface incapable of hiding an outage like this again.
What changed
- Positional cursor + chunked summarization (#248). Checkpoint now maintains a per-project cursor (
file+ byteoffset, strictly validated, safe-side recovery) and digests the backlog in ≤200KB chunks, at most 3 per run, each a committed transaction: summarize → write doc → merge state → advance cursor. A crash redoes at most one chunk — summary ids are deterministic over the content window, so a redo adopts the orphan instead of duplicating it. Verified by a constructed post-crash disk-state matrix in CI plus a live spawned-process SIGKILL smoke (kill at a mid-run chunk boundary → held lock refused conservatively → stale takeover → drain to EOF with no lost turns). - Stats tell instrumented truth (#249).
outcomes_7dcounts only landings,turns_7dcounts turn appends separately, and summarizer failures land in afailedbucket instead of vanishing. - Per-layer freshness (#250). The stats payload gains a
layersblock (per-project capture/digest freshness),um-alert.shescalates on stale layers, and the control page grew a freshness tile. um-drain.sh(#251). The operator loop that turns "3 chunks per run" into "keep going until done": repeated/api/checkpointPOSTs per project with the full response taxonomy, a cost/pending estimate and confirm gate up front,--probe <project>for a one-project rehearsal, and a legacy-server preflight. Session-end 502s are now disambiguated (stage,provider_class).
Compatibility
- Config: five new chunking keys in
checkpoint.json, all env-overridable (UM_CHECKPOINT_CHUNK_MAX_BYTES,UM_CHECKPOINT_MAX_CHUNKS_PER_RUN, …); shipped defaults preserve existing behavior for small backlogs. - API:
/api/checkpointresponses gain additive fields (chunks_done,backlog_remaining,thin_tail,stopped); the deprecatedtruncatedalias is retained this cycle and will be removed in the next major. - No reindex, no migration; the cursor initializes from existing session summaries (or a full digest from zero for never-summarized projects).
Multi-arch image on GHCR (amd64 + arm64), both variants boot-verified from the published registry before release.
v1.15.0 — write-side valid_from stamp
Temporal query-resolution shipped in v1.14.0 ranking on metadata.valid_from and nothing else — and only 167 of 353 live points (47.3%) carried that field. The rest had createdAt alone, which is bulk-arrival time: 86.5% of them land on just two import days. Ranking on it would order half the corpus by which batch it arrived in, so it can never serve as a fallback.
That deficit was a write-path defect. buildPayload stamped createdAt on every add and valid_from on none, so any direct add whose caller supplied no event time — the fact-extraction path above all — landed permanently ungradeable by the temporal window.
This release closes it, forward-only: no backfill, no migration, no reindex.
What changed
- The stamp. Every direct, non-rebuild, non-system add now carries an event time, sharing one clock read with
createdAtsovalid_from === createdAtis byte-exact. isUsableDate— a shared predicate exported fromranking.mjsbesideresolveItemDate, deliberately stricter than the reader. The invariant isaccept(isUsableDate) ⊆ resolvable(resolveItemDate): everything the writer preserves is readable, never the reverse. Without it,[]and'yesterday'would be stored present-but-unreadable, andtrue/1would become 1970 dates.- Rebuild and system-doc paths excluded, so a reindex can only ever preserve an event time, never mint one.
valid_from.missing_on_indexwarn — a doc indexed without an event time is otherwise permanently ungradeable and completely silent. The stamp cannot close that hole; this does.- Frontmatter clobber fixed in
memory_captureandmemory_supersede. Both spread the caller's metadata after the server default, sovalid_from: nullsilently won. valid_fromadded toR6_PROTECTED_KEYS, closing an unguarded caller write to the ranking date viaPUT /v1/memories/:id.- S11 smoke gate for the flat→nested projection — the one property no unit test can reach, since the mapping happens inside mem0ai's
Memory.search.
Read-path behaviour is unchanged under the default configuration. UM_TEMPORAL_QUERY stays false and this release does not touch it.
⚠ Do not adopt yet if you run UM_TEMPORAL_DECAY=true
applyTemporalDecay leaves an undated point's score untouched. Undated points therefore behave as though their decay factor were permanently 1.0, while every stamped point's factor falls below it as it ages. With no backfill, points written before this release keep that advantage indefinitely.
Stated precisely, because the loose version is alarming and wrong:
This is about the factor, not final rank. Decay multiplies the cosine score, so an undated point does not outrank a higher-scoring stamped one — undated at 0.30 still loses to a stamped 0.90 aged 7 days at a 30-day half-life (0.90 × 0.79 = 0.713). The bias is systematic but score-relative.
Two further precisions: a freshly stamped point has factor ≈ 1.0 and ties, so the disadvantage appears as it ages rather than at once. And the undated cohort is not "the oldest" — its createdAt clusters on recent bulk-import days, and treating that as age is exactly the inference this design forbids.
server/.env.example carries this warning at the flag.
The follow-up that closes this arc
The backfill (#223) is what retires the advisory above. It cannot simply copy createdAt — see the 86.5% figure — so it needs a genuine per-point event-time source, and that design does not exist yet. It is specifiable now but blocked from being built until the current measurement frame closes.
Until then, a decay-enabled operator's correct move is to stay on v1.14.0.
Also in this release
The mem0ai patch and source-hash integrity checks now run on every PR without secrets (#232). They previously lived inside the secret-gated smoke job, so dependabot PRs — which run in a separate secrets scope — presented as green with the entire test suite skipped. #211 (mem0ai 3.1.2) was a live example: the source hash drifts and the version-pinned patch does not apply. Reconciliation tracked in #231.
Verification
- Suite 2170 tests / 2155 pass / 0 fail / 15 skipped
- Every guard mutation-verified, and each red control checked for which tests it flips rather than merely that it failed
computeFactIdbyte-identical to the prior release — dedup identity provably unmoved
⏩ Forward-pointer (added 2026-08-10): the advisory above is resolved on main
The undated-point advantage described above is fixed after this release by a read-path policy rather than a backfill — applyTemporalDecay now imputes a fixed factor (exp(-0.25) ≈ 0.78) for a point with no resolvable event time instead of leaving it at 1.0 (PR #236). The backfill this release anticipated was measured out: no surviving source can date more than 22 of the 186 legacy points (11.8%), so #223 closes via the policy, not a migration. Recall safety was measured before and after on a corpus-realistic fixture (perfect retention both sides, doubled runs); the full evidence trail and the production-flip precondition live in #239. The advisory remains accurate for this release — decay-enabled operators should adopt the next release rather than staying on v1.14.0.
v1.14.0 — temporal query-resolution v1 (inert)
Temporal query resolution for recall: a deterministic parser turns a date expression inside a search query ("last week", "in June", "yesterday", "since 2026-05-01") into a date window, and results are re-ranked toward that window instead of by relevance alone. No model call is added to the read path.
Ships inert. The re-ranking path is gated on UM_TEMPORAL_QUERY=true, which is off by default — with the flag unset, search behaves exactly as it did in 1.13.2. What does run by default is a prevalence counter (recall.temporal_query) recording how often queries contain a resolvable date expression at all. That prevalence is the evidence base for deciding whether the feature is worth enabling.
- 12 expression kinds (
on_date,since_date,in_month,last_n,yesterday,today,last_week,this_week,last_month,this_month,last_year,this_year), deterministic parse only — no per-search model call, no added latency on the default path. - The ranking date is
metadata.valid_fromand nothing else.createdAtis deliberately not a fallback: it records bulk-arrival time, so ranking on it would order results by which import batch they happened to land in. - Honest reach today is 47.3% of stored points — the ones carrying
valid_from. The write-side stamp that lifts it is the next change; it heals the corpus forward, with no backfill or migration. - Query input is bounded at 512 chars with ReDoS timing gates; quoted spans are excluded from date matching (apostrophes preserved).
- The mem0-compat facade emits the same prevalence counter but is not re-ranked — a recorded v1 limitation, not an oversight: that path's fetch/limit semantics deserve their own decision.
- Also in this release:
.env.example/install.sh/ OpenAPI surface for the new flag, and the regenerated Custom GPT actions spec.
Deployment note: upgrading needs no configuration change — leave UM_TEMPORAL_QUERY unset to keep current behavior.
Release image: ghcr.io/goldenwo/universal-memory-server (semver tags). See the repository README for setup and upgrade instructions.