Skip to content

Releases: goldenwo/universal-memory

v1.22.0 — Codex CLI session capture + hooks tolerate the per-IP rate limit

Choose a tag to compare

@goldenwo goldenwo released this 10 Sep 13:39
e500b04
  • Codex CLI sessions are now captured (PR #305): the Stop hook parses Codex rollout JSONL (response_item/message user/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 silent skip=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 as error=http-429 instead of server-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.mjs pins 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)

Choose a tag to compare

@goldenwo goldenwo released this 07 Sep 18:53
896edee
  • MCP transport: notifications acknowledged with 202 (PR #302): POST /mcp now answers a JSON-RPC notification (any message without an id, 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 the um server 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 an id) are byte-identical. The OpenAPI /mcp path 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/cancelled202 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

Choose a tag to compare

@goldenwo goldenwo released this 05 Sep 22:58
9a01266
  • #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 while UM_TEMPORAL_DECAY is off (the default). New /api/stats.undated_imputation block (19 snake_case keys; applied_factor is 1 and factor null 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.sh gains 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

Choose a tag to compare

@goldenwo goldenwo released this 02 Sep 15:12
2f77a1a
  • #294 root-named project slugs (PR #295): Project slugs are now named after the project ROOT (.git dominant, UM_PROJECT_MARKERS respected). Existing subdir-named layers are NOT migrated and age out. The only operator action is the normal plugin/install-cli refresh. um-preview from a subdirectory now needs --project. Success hook.log lines carry project=<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 sync now carries supersedes / superseded_by from the ADR's frontmatter into the registered point (label-only — status still derives from the file's own status:; 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

Choose a tag to compare

@goldenwo goldenwo released this 01 Sep 14:09
9cdb323
  • #283 crash-dead capture alarm (PR #291): /api/stats capture entries gain checkpoint_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_status now derives the server-managed point status on the identity path — retired ADRs leave default recall (include_superseded=true recovers them); un-suppression = flip frontmatter + re-sync. Live-verified: ADR-0004 gone from default search post-sync.
  • #285 (PR #288): create-adr.sh gains the ~/.um/auth-token file tier — bare /adr sync auths 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

Choose a tag to compare

@goldenwo goldenwo released this 24 Aug 17:30
195ccb5

v1.18.0 — the capture-anomaly alarm + identity-addressed ADR writes

The alarm (#267, PR #282):

  • stop.sh now self-reports its six measured-zero silent-death reasons as signal.capture_anomaly counter rows via POST /api/capture-anomaly (pure bash+curl helper um_signal_anomaly — a no-python client still reports; 429 carved as http-429 ahead of the server-too-old residual).
  • skip=empty-delta split into -stalled / -filtered (exhaustive shape classification; pass-2 always emits EMPTY).
  • /api/stats exposes an independent anomalies section (fail-isolated to anomalies:null; always-present signals key: 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-old quietly.
  • Residual (crash-dead class) deliberately unalarmed pending its measured base rate — #283.

Identity-addressed ADR writes (#279, PR #281):

  • type:'adr' writes with a usable adr_id get a deterministic point id from (repo_path, adr_id, userId) — idempotent full-replace, dedup bypassed, verbatim title, createdAt preserved, 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

Choose a tag to compare

@goldenwo goldenwo released this 19 Aug 00:34
96abada

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.md valid_from is 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 == 182 alone is the marker (NOT missing covers_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 ci fires 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

Choose a tag to compare

@goldenwo goldenwo released this 17 Aug 03:40
a1691a8

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 + byte offset, 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_7d counts only landings, turns_7d counts turn appends separately, and summarizer failures land in a failed bucket instead of vanishing.
  • Per-layer freshness (#250). The stats payload gains a layers block (per-project capture/digest freshness), um-alert.sh escalates 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/checkpoint POSTs 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/checkpoint responses gain additive fields (chunks_done, backlog_remaining, thin_tail, stopped); the deprecated truncated alias 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

Choose a tag to compare

@goldenwo goldenwo released this 05 Aug 02:09
7129ba1

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 createdAt so valid_from === createdAt is byte-exact.
  • isUsableDate — a shared predicate exported from ranking.mjs beside resolveItemDate, deliberately stricter than the reader. The invariant is accept(isUsableDate) ⊆ resolvable(resolveItemDate): everything the writer preserves is readable, never the reverse. Without it, [] and 'yesterday' would be stored present-but-unreadable, and true/1 would 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_index warn — 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_capture and memory_supersede. Both spread the caller's metadata after the server default, so valid_from: null silently won.
  • valid_from added to R6_PROTECTED_KEYS, closing an unguarded caller write to the ranking date via PUT /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
  • computeFactId byte-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 backfillapplyTemporalDecay 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)

Choose a tag to compare

@goldenwo goldenwo released this 02 Aug 04:07

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_from and nothing else. createdAt is 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.