Skip to content

Conversation

@salacoste
Copy link

@salacoste salacoste commented Jan 5, 2026

Summary

This PR makes OpenMemory easier to run and integrate in multi-project setups (Docker-first) and adds missing MCP operations for updating and deleting memories.

Key changes

  • Docker defaults updated to use port 18080 (configurable via OM_PORT) and improved compose healthchecks.
  • Server now respects platform-provided PORT (with OM_PORT taking precedence).
  • Added multi-project guidance + env templates for per-project instances and optional team/shared setups.
  • VS Code extension defaults aligned with Docker port and improved MCP config generation + project-scoped user_id.
  • MCP tools added: openmemory_update and openmemory_delete (docs updated).
  • MCP HTTP transport now supports GET (SSE stream) and DELETE on /mcp in addition to POST (better compatibility with clients like Claude Code).
  • MCP tools now support a default user scope via OM_DEFAULT_USER_ID / OPENMEMORY_DEFAULT_USER_ID / OPENMEMORY_USER_ID when a client doesn’t provide user_id.
  • IDE context endpoint now supports passing user_id (body) and x-om-user-id / x-openmemory-user-id headers.

Migration notes

  • If you previously used 8080, update URLs/port mappings or set OM_PORT=8080.
  • If you see truncated content, check OM_USE_SUMMARY_ONLY=true (it stores only the summarized extract).

Docs

  • README.md includes Docker + integration guide and MCP tool list.
  • docs/multi-project.md and docs/mcp.md provide detailed workflows.

@nullure
Copy link
Member

nullure commented Jan 5, 2026

Hi salacoste, thank you for your contribution. However, we require mandatory tests for every new pull request. Please conduct the tests and send the results along with the script.

@salacoste
Copy link
Author

Ran the mandatory PR tests locally using the provided script added in this PR.

Script

  • scripts/run-pr-tests.sh
  • Runs Node + Python SDK tests in Docker (node:22-bullseye, python:3.11) and also builds packages/openmemory-js/Dockerfile.

Command

  • ./scripts/run-pr-tests.sh

Results (PASS)

  • Node SDK: packages/openmemory-jsnpm ci + npm run build + npx tsx tests/test_omnibus.ts
  • Python SDK: packages/openmemory-pypip install -e .[dev] + pytest tests/test_omnibus.py -v ✅ (3 passed)
  • Docker build: docker build -t openmemory-prtest:local packages/openmemory-js

The script writes a full log to test-results/pr-tests-<timestamp>.log (example from my run: test-results/pr-tests-20260105T154351Z.log).

@nullure
Copy link
Member

nullure commented Jan 5, 2026

Hi salacoste, i don't see the code of the script. Also the script should explicity test the changes you made, each function of it.

@salacoste
Copy link
Author

salacoste commented Jan 6, 2026

thanks for the review. I ran a local validation that explicitly tests the changes introduced in this PR (MCP/HTTP service + new MCP tools + transport compatibility +
SDK sanity).

Where to find the script (in repo):

  • scripts/run-pr-tests.sh

How to reproduce locally:

  • ./scripts/run-pr-tests.sh

What the script covers:

  • JS SDK sanity: containerized build + packages/openmemory-js/tests/test_omnibus.ts
  • Python SDK sanity: containerized install + pytest packages/openmemory-py/tests/test_omnibus.py
  • Docker image build: packages/openmemory-js/Dockerfile
  • Service checks: /health, auth via x-api-key
  • HTTP API CRUD: POST /memory/add, GET /memory/:id, PATCH /memory/:id, DELETE /memory/:id
  • MCP transport: GET /mcp (SSE headers), POST /mcp (JSON-RPC), DELETE /mcp
  • MCP tools coverage: tools/list includes openmemory_update and openmemory_delete, plus full flow:
    openmemory_store -> openmemory_get -> openmemory_update -> openmemory_get -> openmemory_reinforce -> openmemory_delete -> openmemory_get
  • Default user scoping: verifies MCP calls without user_id use OM_DEFAULT_USER_ID
  • Regression check: large content returned intact via MCP when OM_USE_SUMMARY_ONLY=false
  • Persistence: data survives container restart when using the same volume

Attached to this comment:

  • run.log (full execution log)
  • PR112-validation-report.md (detailed report; includes the full script content and captured request/response artifacts)

PR112-validation-report.md
run.log

PR112 Validation Report

  • Generated (UTC): 2026-01-06T05:22:34Z
  • Commit: b9d003a
  • Script: scripts/run-pr-tests.sh
  • Run dir: /xxx/OpenMemory/test-results/pr112-20260106T052123Z

Test Script (full)

#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOG_DIR="${ROOT_DIR}/test-results"
STAMP="$(date -u +"%Y%m%dT%H%M%SZ")"
RUN_DIR="${LOG_DIR}/pr112-${STAMP}"
LOG_FILE="${RUN_DIR}/run.log"
REPORT_FILE="${LOG_DIR}/PR112-validation-report.md"

mkdir -p "${RUN_DIR}"

exec > >(tee -a "${LOG_FILE}") 2>&1

echo "[pr-tests] start: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "[pr-tests] repo: ${ROOT_DIR}"
echo "[pr-tests] run_dir: ${RUN_DIR}"

require_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    echo "[pr-tests] missing required command: $1" >&2
    exit 127
  fi
}

section() {
  echo
  echo "============================================================"
  echo "[pr-tests] $1"
  echo "============================================================"
}

pick_free_port() {
  python3 - <<'PY'
import socket
for port in range(18081, 18151):
    s = socket.socket()
    try:
        s.bind(("127.0.0.1", port))
        print(port)
        raise SystemExit(0)
    except OSError:
        continue
    finally:
        try:
            s.close()
        except Exception:
            pass
raise SystemExit("no free port in range 18081-18150")
PY
}

expect_http_code() {
  local want="$1"
  local got="$2"
  local label="$3"
  if [[ "${got}" != "${want}" ]]; then
    echo "[pr-tests] ❌ ${label}: expected HTTP ${want}, got ${got}" >&2
    return 1
  fi
  echo "[pr-tests] ✅ ${label}: HTTP ${got}"
}

extract_mcp_session_id() {
  local headers_file="$1"
  python3 - <<'PY' "${headers_file}"
import re, sys
headers = open(sys.argv[1], "r", encoding="utf-8", errors="ignore").read().splitlines()
for line in headers:
    m = re.match(r"(?i)^mcp-session-id:\\s*(.+?)\\s*$", line.strip())
    if m:
        print(m.group(1).strip())
        raise SystemExit(0)
print("")
raise SystemExit(0)
PY
}

json_extract() {
  local file="$1"
  local expr="$2"
  python3 - <<'PY' "${file}" "${expr}"
import json, sys
data = json.load(open(sys.argv[1], "r", encoding="utf-8"))
expr = sys.argv[2].strip()

def get_path(obj, path):
    cur = obj
    for part in path.split("."):
        if part.endswith("]"):
            name, idx = part[:-1].split("[", 1)
            if name:
                cur = cur[name]
            cur = cur[int(idx)]
        else:
            cur = cur[part]
    return cur

val = get_path(data, expr)
if isinstance(val, (dict, list)):
    print(json.dumps(val))
else:
    print(val)
PY
}

run_in_docker_node() {
  local image="node:22-bullseye"
  docker run --rm \
    -v "${ROOT_DIR}:/repo" \
    -w "/repo/packages/openmemory-js" \
    -e OM_DB_URL="sqlite:///:memory:" \
    -e OM_TIER="fast" \
    -e OM_VEC_DIM="1536" \
    "${image}" \
    bash -lc 'set -euo pipefail; apt-get update -y >/dev/null; apt-get install -y python3 make g++ >/dev/null; node -v; npm -v; npm ci; npm run build; npx tsx tests/test_omnibus.ts'
}

run_in_docker_python() {
  local image="python:3.11"
  docker run --rm \
    -v "${ROOT_DIR}:/repo" \
    -w "/repo/packages/openmemory-py" \
    "${image}" \
    bash -lc 'set -euo pipefail; python --version; pip --version; pip install -e ".[dev]"; pytest tests/test_omnibus.py -v'
}

run_mcp_http_smoke() {
  local image_tag="$1"

  local api_key="pr112-test-key"
  local default_user="pr112-user"
  local host_port
  host_port="$(pick_free_port)"

  local container_port="18080"
  local container_name="openmemory-pr112-${STAMP}"
  local volume_name="openmemory-pr112-data-${STAMP}"

  section "Run container for MCP/HTTP smoke (port ${host_port} -> ${container_port})"
  echo "[pr-tests] building/running with api_key=${api_key} default_user=${default_user}"

  # Persist identifiers outside of function scope because EXIT traps run
  # after locals are unset (macOS bash + set -u).
  PRTEST_CONTAINER_NAME="${container_name}"
  PRTEST_VOLUME_NAME="${volume_name}"

  docker volume create "${volume_name}" >/dev/null

  cleanup() {
    if [[ "${KEEP_DOCKER:-}" == "1" ]]; then
      echo "[pr-tests] KEEP_DOCKER=1 set; skipping container/volume cleanup"
      return
    fi
    if [[ -n "${PRTEST_CONTAINER_NAME:-}" ]]; then
      docker rm -f "${PRTEST_CONTAINER_NAME}" >/dev/null 2>&1 || true
    fi
    if [[ -n "${PRTEST_VOLUME_NAME:-}" ]]; then
      docker volume rm -f "${PRTEST_VOLUME_NAME}" >/dev/null 2>&1 || true
    fi
  }
  trap cleanup EXIT

  docker run -d --rm \
    --name "${container_name}" \
    -e "OM_PORT=${container_port}" \
    -e "OM_API_KEY=${api_key}" \
    -e "OM_DEFAULT_USER_ID=${default_user}" \
    -e "OM_USE_SUMMARY_ONLY=false" \
    -e "OM_MAX_PAYLOAD_SIZE=1048576" \
    -e "OM_MODE=standard" \
    -e "OM_TIER=hybrid" \
    -e "OM_EMBEDDINGS=synthetic" \
    -e "OM_EMBEDDING_FALLBACK=synthetic" \
    -e "OM_METADATA_BACKEND=sqlite" \
    -e "OM_VECTOR_BACKEND=sqlite" \
    -e "OM_DB_PATH=/data/openmemory.sqlite" \
    -v "${volume_name}:/data" \
    -p "${host_port}:${container_port}" \
    "${image_tag}" >/dev/null

  local base="http://127.0.0.1:${host_port}"

  section "Wait for /health"
  local deadline=$((SECONDS + 60))
  until curl -fsS "${base}/health" >/dev/null 2>&1; do
    if (( SECONDS > deadline )); then
      echo "[pr-tests] ❌ healthcheck timeout; container logs:" >&2
      docker logs "${container_name}" >&2 || true
      exit 1
    fi
    sleep 1
  done
  curl -fsS "${base}/health" | tee "${RUN_DIR}/health.json" >/dev/null
  echo "[pr-tests] ✅ healthy: ${base}"

  section "Auth required (HTTP)"
  local code
  code="$(curl -sS -o /dev/null -w "%{http_code}" "${base}/memory/all")"
  expect_http_code "401" "${code}" "GET /memory/all without key"

  code="$(curl -sS -o /dev/null -w "%{http_code}" -H "x-api-key: ${api_key}" "${base}/memory/all")"
  expect_http_code "200" "${code}" "GET /memory/all with key"

  section "HTTP CRUD: add/get/patch/delete"
  curl -sS -H "Content-Type: application/json" -H "x-api-key: ${api_key}" \
    --data "$(python3 - <<'PY'
import json
print(json.dumps({
  "content": "http-add-content",
  "tags": ["pr112", "http"],
  "metadata": {"source": "pr112-run-pr-tests"},
  "user_id": "pr112-user",
}))
PY
)" \
    "${base}/memory/add" | tee "${RUN_DIR}/http-memory-add.json" >/dev/null

  local http_id
  http_id="$(python3 - <<'PY' "${RUN_DIR}/http-memory-add.json"
import json, sys
data = json.load(open(sys.argv[1]))
print(data["id"])
PY
)"
  echo "[pr-tests] http memory id: ${http_id}"

  curl -sS -H "x-api-key: ${api_key}" \
    "${base}/memory/${http_id}?user_id=${default_user}" | tee "${RUN_DIR}/http-memory-get.json" >/dev/null

  curl -sS -X PATCH -H "Content-Type: application/json" -H "x-api-key: ${api_key}" \
    --data "$(python3 - <<'PY'
import json
print(json.dumps({
  "content": "http-updated-content",
  "tags": ["pr112", "http", "updated"],
  "metadata": {"source": "pr112-run-pr-tests", "updated": True},
  "user_id": "pr112-user",
}))
PY
)" \
    "${base}/memory/${http_id}" | tee "${RUN_DIR}/http-memory-patch.json" >/dev/null

  curl -sS -H "x-api-key: ${api_key}" \
    "${base}/memory/${http_id}?user_id=${default_user}" | tee "${RUN_DIR}/http-memory-get-after-patch.json" >/dev/null

  local http_content
  http_content="$(python3 - <<'PY' "${RUN_DIR}/http-memory-get-after-patch.json"
import json, sys
data = json.load(open(sys.argv[1]))
print(data.get("content",""))
PY
)"
  if [[ "${http_content}" != "http-updated-content" ]]; then
    echo "[pr-tests] ❌ HTTP PATCH did not update content (got '${http_content}')" >&2
    exit 1
  fi
  echo "[pr-tests] ✅ HTTP PATCH updated content"

  curl -sS -X DELETE -H "x-api-key: ${api_key}" \
    "${base}/memory/${http_id}?user_id=${default_user}" | tee "${RUN_DIR}/http-memory-delete.json" >/dev/null

  code="$(curl -sS -o /dev/null -w "%{http_code}" -H "x-api-key: ${api_key}" "${base}/memory/${http_id}?user_id=${default_user}")"
  expect_http_code "404" "${code}" "GET /memory/:id after delete"

  section "MCP transport: GET /mcp (SSE headers)"
  set +e
  curl -sS -D "${RUN_DIR}/mcp-sse.headers" -o /dev/null \
    --max-time 2 \
    -H "Accept: text/event-stream" \
    -H "x-api-key: ${api_key}" \
    "${base}/mcp"
  local curl_rc=$?
  set -e
  if [[ "${curl_rc}" -ne 0 && "${curl_rc}" -ne 18 && "${curl_rc}" -ne 28 ]]; then
    echo "[pr-tests] ❌ SSE probe curl failed with rc=${curl_rc}" >&2
    exit 1
  fi

  python3 - <<'PY' "${RUN_DIR}/mcp-sse.headers"
import sys
hdr = open(sys.argv[1], "r", encoding="utf-8", errors="ignore").read().lower()
assert "200" in hdr.splitlines()[0]
assert "content-type:" in hdr
assert "text/event-stream" in hdr
print("ok")
PY
  echo "[pr-tests] ✅ SSE headers OK"

  section "MCP initialize + tools/list"
  curl -sS -D "${RUN_DIR}/mcp-init.headers" -o "${RUN_DIR}/mcp-init.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    --data "$(python3 - <<'PY'
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":1,
  "method":"initialize",
  "params":{
    "protocolVersion":"2024-11-05",
    "capabilities":{},
    "clientInfo":{"name":"pr112-run-pr-tests","version":"0.0.0"}
  }
}))
PY
)" \
    "${base}/mcp"

  local mcp_sid
  mcp_sid="$(extract_mcp_session_id "${RUN_DIR}/mcp-init.headers")"
  if [[ -n "${mcp_sid}" ]]; then
    echo "[pr-tests] mcp-session-id: ${mcp_sid}"
  else
    echo "[pr-tests] mcp-session-id: (none; stateless transport)"
  fi

  mcp_session_header() {
    if [[ -n "${mcp_sid}" ]]; then
      printf '%s\n' "-H" "mcp-session-id: ${mcp_sid}"
    fi
  }

  curl -sS -D "${RUN_DIR}/mcp-tools.headers" -o "${RUN_DIR}/mcp-tools.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
    "${base}/mcp"

  python3 - <<'PY' "${RUN_DIR}/mcp-tools.json"
import json, sys
data = json.load(open(sys.argv[1]))
names = [t["name"] for t in data["result"]["tools"]]
need = ["openmemory_store","openmemory_query","openmemory_list","openmemory_get","openmemory_reinforce","openmemory_update","openmemory_delete"]
missing = [n for n in need if n not in names]
if missing:
    raise SystemExit("missing tools: " + ", ".join(missing))
print("ok")
PY
  echo "[pr-tests] ✅ tools/list includes update/delete"

  section "MCP: store (default user_id from env) -> get -> update -> get -> reinforce -> delete -> get"
  curl -sS -o "${RUN_DIR}/mcp-store.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<'PY'
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":3,
  "method":"tools/call",
  "params":{
    "name":"openmemory_store",
    "arguments":{
      "content":"mcp-store-content",
      "tags":["pr112","mcp"],
      "metadata":{"source":"pr112-run-pr-tests"}
    }
  }
}))
PY
)" \
    "${base}/mcp"

  local mcp_store_json
  mcp_store_json="$(json_extract "${RUN_DIR}/mcp-store.json" "result.content[1].text")"
  echo "${mcp_store_json}" > "${RUN_DIR}/mcp-store.payload.json"
  local mcp_id
  mcp_id="$(python3 - <<'PY' "${RUN_DIR}/mcp-store.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
print(payload["hsg"]["id"])
PY
)"
  local mcp_user
  mcp_user="$(python3 - <<'PY' "${RUN_DIR}/mcp-store.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
print(payload.get("user_id") or "")
PY
)"
  echo "[pr-tests] stored mcp id: ${mcp_id} (user_id='${mcp_user}')"
  if [[ "${mcp_user}" != "${default_user}" ]]; then
    echo "[pr-tests] ❌ MCP default user_id mismatch (expected '${default_user}', got '${mcp_user}')" >&2
    exit 1
  fi
  echo "[pr-tests] ✅ MCP store used default user_id"

  curl -sS -o "${RUN_DIR}/mcp-get.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":4,
  "method":"tools/call",
  "params":{"name":"openmemory_get","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
    "${base}/mcp"

  local mcp_get_payload
  mcp_get_payload="$(json_extract "${RUN_DIR}/mcp-get.json" "result.content[0].text")"
  echo "${mcp_get_payload}" > "${RUN_DIR}/mcp-get.payload.json"
  python3 - <<'PY' "${RUN_DIR}/mcp-get.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
assert payload["content"] == "mcp-store-content"
print("ok")
PY
  echo "[pr-tests] ✅ MCP get returned full content"

  section "MCP: large content is not truncated (OM_USE_SUMMARY_ONLY=false)"
  python3 - <<'PY' > "${RUN_DIR}/mcp-big-content.txt"
tail = "<<<TAIL-MUST-SURVIVE>>>"
print(("x" * 20000) + tail)
PY

  curl -sS -o "${RUN_DIR}/mcp-store-big.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
big = open("${RUN_DIR}/mcp-big-content.txt", "r").read()
print(json.dumps({
  "jsonrpc":"2.0",
  "id":41,
  "method":"tools/call",
  "params":{
    "name":"openmemory_store",
    "arguments":{
      "content": big,
      "tags":["pr112","mcp","big"],
      "metadata":{"source":"pr112-run-pr-tests","kind":"big"}
    }
  }
}))
PY
)" \
    "${base}/mcp"

  local mcp_big_store_json
  mcp_big_store_json="$(json_extract "${RUN_DIR}/mcp-store-big.json" "result.content[1].text")"
  echo "${mcp_big_store_json}" > "${RUN_DIR}/mcp-store-big.payload.json"
  local mcp_big_id
  mcp_big_id="$(python3 - <<'PY' "${RUN_DIR}/mcp-store-big.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
print(payload["hsg"]["id"])
PY
)"

  curl -sS -o "${RUN_DIR}/mcp-get-big.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":42,
  "method":"tools/call",
  "params":{"name":"openmemory_get","arguments":{"id":"${mcp_big_id}"}}
}))
PY
)" \
    "${base}/mcp"

  local mcp_big_payload
  mcp_big_payload="$(json_extract "${RUN_DIR}/mcp-get-big.json" "result.content[0].text")"
  echo "${mcp_big_payload}" > "${RUN_DIR}/mcp-get-big.payload.json"
  python3 - <<'PY' "${RUN_DIR}/mcp-get-big.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
content = payload["content"]
assert content.rstrip("\n").endswith("<<<TAIL-MUST-SURVIVE>>>")
assert len(content) > 20000
print("ok")
PY
  echo "[pr-tests] ✅ MCP get preserved big content tail"

  curl -sS -o "${RUN_DIR}/mcp-delete-big.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":43,
  "method":"tools/call",
  "params":{"name":"openmemory_delete","arguments":{"id":"${mcp_big_id}"}}
}))
PY
)" \
    "${base}/mcp"

  curl -sS -o "${RUN_DIR}/mcp-update.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":5,
  "method":"tools/call",
  "params":{
    "name":"openmemory_update",
    "arguments":{
      "id":"${mcp_id}",
      "content":"mcp-updated-content",
      "tags":["pr112","mcp","updated"],
      "metadata":{"source":"pr112-run-pr-tests","updated":True}
    }
  }
}))
PY
)" \
    "${base}/mcp"

  curl -sS -o "${RUN_DIR}/mcp-get-after-update.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":6,
  "method":"tools/call",
  "params":{"name":"openmemory_get","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
    "${base}/mcp"

  local mcp_get2_payload
  mcp_get2_payload="$(json_extract "${RUN_DIR}/mcp-get-after-update.json" "result.content[0].text")"
  echo "${mcp_get2_payload}" > "${RUN_DIR}/mcp-get-after-update.payload.json"
  python3 - <<'PY' "${RUN_DIR}/mcp-get-after-update.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
assert payload["content"] == "mcp-updated-content"
assert payload["metadata"]["updated"] is True
assert "updated" in payload["tags"]
print("ok")
PY
  echo "[pr-tests] ✅ MCP update applied (content/tags/metadata)"

  curl -sS -o "${RUN_DIR}/mcp-reinforce.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":7,
  "method":"tools/call",
  "params":{"name":"openmemory_reinforce","arguments":{"id":"${mcp_id}","boost":0.2}}
}))
PY
)" \
    "${base}/mcp"

  curl -sS -o "${RUN_DIR}/mcp-delete.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":8,
  "method":"tools/call",
  "params":{"name":"openmemory_delete","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
    "${base}/mcp"

  curl -sS -o "${RUN_DIR}/mcp-get-after-delete.json" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    --data "$(python3 - <<PY
import json
print(json.dumps({
  "jsonrpc":"2.0",
  "id":9,
  "method":"tools/call",
  "params":{"name":"openmemory_get","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
    "${base}/mcp"

  local after_delete_text
  after_delete_text="$(json_extract "${RUN_DIR}/mcp-get-after-delete.json" "result.content[0].text")"
  if [[ "${after_delete_text}" != *"not found"* ]]; then
    echo "[pr-tests] ❌ MCP get after delete did not report not found" >&2
    echo "[pr-tests] payload: ${after_delete_text}" >&2
    exit 1
  fi
  echo "[pr-tests] ✅ MCP delete removed memory"

  section "MCP transport: DELETE /mcp closes session"
  code="$(curl -sS -o /dev/null -w "%{http_code}" -X DELETE \
    -H "Accept: application/json, text/event-stream" \
    -H "x-api-key: ${api_key}" \
    $(mcp_session_header) \
    "${base}/mcp")"
  if [[ "${code}" != "200" && "${code}" != "204" ]]; then
    echo "[pr-tests] ❌ DELETE /mcp unexpected HTTP ${code}" >&2
    exit 1
  fi
  echo "[pr-tests] ✅ DELETE /mcp returned HTTP ${code}"

  section "Persistence: restart container keeps stored memories"
  curl -sS -H "Content-Type: application/json" -H "x-api-key: ${api_key}" \
    --data "$(python3 - <<'PY'
import json
print(json.dumps({
  "content": "persistence-check-content",
  "tags": ["pr112", "persist"],
  "metadata": {"source": "pr112-run-pr-tests"},
  "user_id": "pr112-user",
}))
PY
)" \
    "${base}/memory/add" | tee "${RUN_DIR}/persist-add.json" >/dev/null
  local persist_id
  persist_id="$(python3 - <<'PY' "${RUN_DIR}/persist-add.json"
import json, sys
data = json.load(open(sys.argv[1]))
print(data["id"])
PY
)"

  docker rm -f "${container_name}" >/dev/null
  docker run -d --rm \
    --name "${container_name}" \
    -e "OM_PORT=${container_port}" \
    -e "OM_API_KEY=${api_key}" \
    -e "OM_DEFAULT_USER_ID=${default_user}" \
    -e "OM_USE_SUMMARY_ONLY=false" \
    -e "OM_MAX_PAYLOAD_SIZE=1048576" \
    -e "OM_MODE=standard" \
    -e "OM_TIER=hybrid" \
    -e "OM_EMBEDDINGS=synthetic" \
    -e "OM_EMBEDDING_FALLBACK=synthetic" \
    -e "OM_METADATA_BACKEND=sqlite" \
    -e "OM_VECTOR_BACKEND=sqlite" \
    -e "OM_DB_PATH=/data/openmemory.sqlite" \
    -v "${volume_name}:/data" \
    -p "${host_port}:${container_port}" \
    "${image_tag}" >/dev/null

  deadline=$((SECONDS + 60))
  until curl -fsS "${base}/health" >/dev/null 2>&1; do
    if (( SECONDS > deadline )); then
      echo "[pr-tests] ❌ healthcheck timeout after restart; container logs:" >&2
      docker logs "${container_name}" >&2 || true
      exit 1
    fi
    sleep 1
  done

  code="$(curl -sS -o /dev/null -w "%{http_code}" -H "x-api-key: ${api_key}" "${base}/memory/${persist_id}?user_id=${default_user}")"
  expect_http_code "200" "${code}" "GET /memory/:id after restart (persistence)"

  curl -sS -H "x-api-key: ${api_key}" \
    "${base}/memory/${persist_id}?user_id=${default_user}" | tee "${RUN_DIR}/persist-get-after-restart.json" >/dev/null

  python3 - <<'PY' "${RUN_DIR}/persist-get-after-restart.json"
import json, sys
data = json.load(open(sys.argv[1]))
assert data.get("content") == "persistence-check-content"
print("ok")
PY
  echo "[pr-tests] ✅ persistence content verified"
}

write_report() {
  section "Write Markdown report"
  local big_len="unknown"
  if [[ -f "${RUN_DIR}/mcp-big-content.txt" ]]; then
    big_len="$(python3 -c "print(len(open('${RUN_DIR}/mcp-big-content.txt','r').read()))")"
  fi
  {
    echo "# PR112 Validation Report"
    echo
    echo "- Generated (UTC): $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
    echo "- Repo: ${ROOT_DIR}"
    echo "- Commit: $(git -C "${ROOT_DIR}" rev-parse HEAD)"
    echo "- Script: scripts/run-pr-tests.sh"
    echo "- Run dir: ${RUN_DIR}"
    echo
    echo "## Test Script (full)"
    echo
    echo "\`\`\`bash"
    cat "${ROOT_DIR}/scripts/run-pr-tests.sh"
    echo "\`\`\`"
    echo
    echo "## What Was Tested"
    echo
    echo "### SDK (containerized)"
    echo "- Node SDK omnibus: build + tests/test_omnibus.ts"
    echo "- Python SDK omnibus: pytest tests/test_omnibus.py"
    echo
    echo "### Service (Docker image)"
    echo "- Build image from packages/openmemory-js/Dockerfile"
    echo "- Start container + /health"
    echo "- Auth required for HTTP API"
    echo "- HTTP CRUD: /memory/add, /memory/:id (GET/PATCH/DELETE)"
    echo "- MCP transport: GET SSE headers, POST JSON-RPC, DELETE /mcp"
    echo "- MCP tools: tools/list includes update/delete; store/get/update/reinforce/delete flow"
    echo "- Default user_id from OM_DEFAULT_USER_ID for MCP calls without user_id"
    echo "- Persistence: restart container with same volume keeps data"
    echo
    echo "## Results (artifacts)"
    echo
    echo "- Log: ${LOG_FILE}"
    echo "- Directory with captured responses: ${RUN_DIR}/"
    echo
    echo "### Health"
    echo "\`\`\`json"
    cat "${RUN_DIR}/health.json"
    echo "\`\`\`"
    echo
    echo "### HTTP CRUD"
    echo "**Add** \`${RUN_DIR}/http-memory-add.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/http-memory-add.json"
    echo "\`\`\`"
    echo
    echo "**Get (before patch)** \`${RUN_DIR}/http-memory-get.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/http-memory-get.json"
    echo "\`\`\`"
    echo
    echo "**Patch** \`${RUN_DIR}/http-memory-patch.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/http-memory-patch.json"
    echo "\`\`\`"
    echo
    echo "**Get (after patch)** \`${RUN_DIR}/http-memory-get-after-patch.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/http-memory-get-after-patch.json"
    echo "\`\`\`"
    echo
    echo "**Delete** \`${RUN_DIR}/http-memory-delete.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/http-memory-delete.json"
    echo "\`\`\`"
    echo
    echo "### MCP (SSE headers probe)"
    echo "\`\`\`text"
    sed -n '1,30p' "${RUN_DIR}/mcp-sse.headers"
    echo "\`\`\`"
    echo
    echo "### MCP initialize"
    echo "**Headers** \`${RUN_DIR}/mcp-init.headers\`"
    echo "\`\`\`text"
    sed -n '1,50p' "${RUN_DIR}/mcp-init.headers"
    echo "\`\`\`"
    echo
    echo "**Body** \`${RUN_DIR}/mcp-init.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-init.json"
    echo "\`\`\`"
    echo
    echo "### MCP tools/list"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-tools.json"
    echo "\`\`\`"
    echo
    echo "### MCP store/get/update/delete (small content)"
    echo "**Store response** \`${RUN_DIR}/mcp-store.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-store.json"
    echo "\`\`\`"
    echo
    echo "**Store payload (parsed)** \`${RUN_DIR}/mcp-store.payload.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-store.payload.json"
    echo "\`\`\`"
    echo
    echo "**Get payload (parsed)** \`${RUN_DIR}/mcp-get.payload.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-get.payload.json"
    echo "\`\`\`"
    echo
    echo "**Update response** \`${RUN_DIR}/mcp-update.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-update.json"
    echo "\`\`\`"
    echo
    echo "**Get-after-update payload (parsed)** \`${RUN_DIR}/mcp-get-after-update.payload.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-get-after-update.payload.json"
    echo "\`\`\`"
    echo
    echo "**Reinforce response** \`${RUN_DIR}/mcp-reinforce.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-reinforce.json"
    echo "\`\`\`"
    echo
    echo "**Delete response** \`${RUN_DIR}/mcp-delete.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-delete.json"
    echo "\`\`\`"
    echo
    echo "**Get-after-delete response** \`${RUN_DIR}/mcp-get-after-delete.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/mcp-get-after-delete.json"
    echo "\`\`\`"
    echo
    echo "### MCP large content (truncation regression check)"
    echo "- Stored content length: ${big_len}"
    echo "- Retrieved payload file (contains full content): ${RUN_DIR}/mcp-get-big.payload.json"
    echo "- Store response: ${RUN_DIR}/mcp-store-big.json"
    echo "- Get response: ${RUN_DIR}/mcp-get-big.json"
    echo
    echo "### Persistence (volume survives restart)"
    echo "**Add** \`${RUN_DIR}/persist-add.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/persist-add.json"
    echo "\`\`\`"
    echo
    echo "**Get after restart** \`${RUN_DIR}/persist-get-after-restart.json\`"
    echo "\`\`\`json"
    cat "${RUN_DIR}/persist-get-after-restart.json"
    echo "\`\`\`"
    echo
    echo "## How To Reproduce"
    echo
    echo "\`\`\`bash"
    echo "scripts/run-pr-tests.sh"
    echo "\`\`\`"
  } > "${REPORT_FILE}"
  echo "[pr-tests] ✅ report: ${REPORT_FILE}"
}

section "Versions"
require_cmd git
require_cmd python3
git -C "${ROOT_DIR}" rev-parse HEAD
git -C "${ROOT_DIR}" status -sb

if command -v docker >/dev/null 2>&1; then
  docker version
else
  echo "[pr-tests] docker not found; cannot run containerized tests." >&2
  exit 127
fi

section "Node SDK (packages/openmemory-js)"
run_in_docker_node

section "Python SDK (packages/openmemory-py)"
run_in_docker_python

section "Docker Build (packages/openmemory-js/Dockerfile)"
docker build -t openmemory-prtest:local "${ROOT_DIR}/packages/openmemory-js"

section "MCP/HTTP Smoke (Docker image)"
require_cmd curl
run_mcp_http_smoke "openmemory-prtest:local"

write_report

section "Done"
echo "[pr-tests] ✅ all checks passed"
echo "[pr-tests] log: ${LOG_FILE}"
echo "[pr-tests] report: ${REPORT_FILE}"

What Was Tested

SDK (containerized)

  • Node SDK omnibus: build + tests/test_omnibus.ts
  • Python SDK omnibus: pytest tests/test_omnibus.py

Service (Docker image)

  • Build image from packages/openmemory-js/Dockerfile
  • Start container + /health
  • Auth required for HTTP API
  • HTTP CRUD: /memory/add, /memory/:id (GET/PATCH/DELETE)
  • MCP transport: GET SSE headers, POST JSON-RPC, DELETE /mcp
  • MCP tools: tools/list includes update/delete; store/get/update/reinforce/delete flow
  • Default user_id from OM_DEFAULT_USER_ID for MCP calls without user_id
  • Persistence: restart container with same volume keeps data

Results (artifacts)

  • Log: /xxx/OpenMemory/test-results/pr112-20260106T052123Z/run.log
  • Directory with captured responses: /xxx/OpenMemory/test-results/pr112-20260106T052123Z/

Health

{"ok":true,"version":"2.0-hsg-tiered","embedding":{"provider":"synthetic","fallback_chain":["synthetic"],"dimensions":256,"mode":"simple","batch_support":false,"advanced_parallel":false,"embed_delay_ms":200,"configured":true,"type":"synthetic"},"tier":"hybrid","dim":256,"cache":3,"expected":{"recall":98,"qps":"700-800","ram":"0.5gb/10k","use":"For high accuracy"}}```

### HTTP CRUD
**Add** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-add.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","primary_sector":"semantic","sectors":["semantic"],"chunks":1}```

**Get (before patch)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-get.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","content":"http-add-content","primary_sector":"semantic","sectors":["semantic"],"tags":["pr112","http"],"metadata":{"source":"pr112-run-pr-tests"},"created_at":1767676950407,"updated_at":1767676950407,"last_seen_at":1767676950407,"salience":0.4,"decay_lambda":0.005,"version":1,"user_id":"pr112-user"}```

**Patch** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-patch.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","updated":true}```

**Get (after patch)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-get-after-patch.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","content":"http-updated-content","primary_sector":"semantic","sectors":["semantic"],"tags":["pr112","http","updated"],"metadata":{"source":"pr112-run-pr-tests","updated":true},"created_at":1767676950407,"updated_at":1767676950490,"last_seen_at":1767676950407,"salience":0.4,"decay_lambda":0.005,"version":2,"user_id":"pr112-user"}```

**Delete** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-delete.json`
```json
{"ok":true}```

### MCP (SSE headers probe)
```text
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,DELETE,OPTIONS
Access-Control-Allow-Headers: Content-Type,Authorization,x-api-key,mcp-session-id,mcp-protocol-version,last-event-id
Access-Control-Expose-Headers: mcp-session-id
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
Date: Tue, 06 Jan 2026 05:22:30 GMT
Transfer-Encoding: chunked

MCP initialize

Headers /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-init.headers

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,DELETE,OPTIONS
Access-Control-Allow-Headers: Content-Type,Authorization,x-api-key,mcp-session-id,mcp-protocol-version,last-event-id
Access-Control-Expose-Headers: mcp-session-id
Content-Type: application/json
Date: Tue, 06 Jan 2026 05:22:32 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

Body /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-init.json

{"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true},"resources":{"listChanged":true},"logging":{},"completions":{}},"serverInfo":{"name":"openmemory-mcp","version":"2.1.0"}},"jsonrpc":"2.0","id":1}```

### MCP tools/list
```json
{"result":{"tools":[{"name":"openmemory_query","description":"Query OpenMemory for contextual memories (HSG) and/or temporal facts","inputSchema":{"type":"object","properties":{"query":{"type":"string","minLength":1,"description":"Free-form search text"},"type":{"type":"string","enum":["contextual","factual","unified"],"default":"contextual","description":"Query type: 'contextual' for HSG semantic search (default), 'factual' for temporal fact queries, 'unified' for both"},"fact_pattern":{"type":"object","properties":{"subject":{"type":"string","description":"Subject pattern (entity) - use undefined for wildcard"},"predicate":{"type":"string","description":"Predicate pattern (relationship) - use undefined for wildcard"},"object":{"type":"string","description":"Object pattern (value) - use undefined for wildcard"}},"additionalProperties":false,"description":"Fact pattern for temporal queries. Used when type is 'factual' or 'unified'"},"at":{"type":"string","description":"ISO date string for point-in-time queries (default: now). Queries facts valid at this time"},"k":{"type":"integer","minimum":1,"maximum":32,"default":8,"description":"Maximum results to return (for HSG queries)"},"sector":{"type":"string","enum":["episodic","semantic","procedural","emotional","reflective"],"description":"Restrict search to a specific sector (for HSG queries)"},"min_salience":{"type":"number","minimum":0,"maximum":1,"description":"Minimum salience threshold (for HSG queries)"},"user_id":{"type":"string","minLength":1,"description":"Isolate results to a specific user identifier"}},"required":["query"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_store","description":"Persist new content into OpenMemory (HSG contextual memory and/or temporal facts)","inputSchema":{"type":"object","properties":{"content":{"type":"string","minLength":1,"description":"Raw memory text to store"},"type":{"type":"string","enum":["contextual","factual","both"],"default":"contextual","description":"Storage type: 'contextual' for HSG only (default), 'factual' for temporal facts only, 'both' for both systems"},"facts":{"type":"array","items":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"description":"Fact subject (entity)"},"predicate":{"type":"string","minLength":1,"description":"Fact predicate (relationship)"},"object":{"type":"string","minLength":1,"description":"Fact object (value)"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Confidence score (0-1, default 1.0)"},"valid_from":{"type":"string","description":"ISO date string for fact validity start (default: now)"}},"required":["subject","predicate","object"],"additionalProperties":false},"description":"Array of facts to store in temporal graph. Required when type is 'factual' or 'both'"},"tags":{"type":"array","items":{"type":"string"},"description":"Optional tag list (for HSG storage)"},"metadata":{"type":"object","additionalProperties":{},"description":"Arbitrary metadata blob"},"user_id":{"type":"string","minLength":1,"description":"Associate the memory with a specific user identifier"}},"required":["content"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_reinforce","description":"Boost salience for an existing memory","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to reinforce"},"boost":{"type":"number","minimum":0.01,"maximum":1,"default":0.1,"description":"Salience boost amount (default 0.1)"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_list","description":"List recent memories for quick inspection","inputSchema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"default":10,"description":"Number of memories to return"},"sector":{"type":"string","enum":["episodic","semantic","procedural","emotional","reflective"],"description":"Optionally limit to a sector"},"user_id":{"type":"string","minLength":1,"description":"Restrict results to a specific user identifier"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_get","description":"Fetch a single memory by identifier","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to load"},"include_vectors":{"type":"boolean","default":false,"description":"Include sector vector metadata"},"user_id":{"type":"string","minLength":1,"description":"Validate ownership against a specific user identifier"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_update","description":"Update an existing memory (content, tags, metadata)","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to update"},"content":{"type":"string","minLength":1,"description":"New memory content (omit to keep current)"},"tags":{"type":"array","items":{"type":"string"},"description":"Replace tags (omit to keep current)"},"metadata":{"type":"object","additionalProperties":{},"description":"Replace metadata (omit to keep current)"},"user_id":{"type":"string","minLength":1,"description":"Validate ownership against a specific user identifier"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_delete","description":"Delete a memory by identifier","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to delete"},"user_id":{"type":"string","minLength":1,"description":"Validate ownership against a specific user identifier"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]},"jsonrpc":"2.0","id":2}```

### MCP store/get/update/delete (small content)
**Store response** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-store.json`
```json
{"result":{"content":[{"type":"text","text":"Stored memory 3872378d-8561-4ac4-bc6b-bfacc31d320f (primary=semantic) across sectors: semantic [user=pr112-user]"},{"type":"text","text":"{\n  \"type\": \"contextual\",\n  \"hsg\": {\n    \"id\": \"3872378d-8561-4ac4-bc6b-bfacc31d320f\",\n    \"primary_sector\": \"semantic\",\n    \"sectors\": [\n      \"semantic\"\n    ]\n  },\n  \"user_id\": \"pr112-user\"\n}"}"}```

**Store payload (parsed)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-store.payload.json`
```json
{
  "type": "contextual",
  "hsg": {
    "id": "3872378d-8561-4ac4-bc6b-bfacc31d320f",
    "primary_sector": "semantic",
    "sectors": [
      "semantic"
    ]
  },
  "user_id": "pr112-user"
}

Get payload (parsed) /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get.payload.json

{
  "id": "3872378d-8561-4ac4-bc6b-bfacc31d320f",
  "content": "mcp-store-content",
  "primary_sector": "semantic",
  "salience": 0.4,
  "decay_lambda": 0.005,
  "created_at": 1767676952745,
  "updated_at": 1767676952745,
  "last_seen_at": 1767676952745,
  "user_id": "pr112-user",
  "tags": [
    "pr112",
    "mcp"
  ],
  "metadata": {
    "source": "pr112-run-pr-tests"
  }
}

Update response /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-update.json

{"result":{"content":[{"type":"text","text":"Updated memory 3872378d-8561-4ac4-bc6b-bfacc31d320f"},{"type":"text","text":"{\n  \"id\": \"3872378d-8561-4ac4-bc6b-bfacc31d320f\",\n  \"updated\": true,\n  \"user_id\": \"pr112-user\"\n}"}]},"jsonrpc":"2.0","id":5}```

**Get-after-update payload (parsed)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-after-update.payload.json`
```json
{
  "id": "3872378d-8561-4ac4-bc6b-bfacc31d320f",
  "content": "mcp-updated-content",
  "primary_sector": "semantic",
  "salience": 0.4,
  "decay_lambda": 0.005,
  "created_at": 1767676952745,
  "updated_at": 1767676953217,
  "last_seen_at": 1767676952745,
  "user_id": "pr112-user",
  "tags": [
    "pr112",
    "mcp",
    "updated"
  ],
  "metadata": {
    "source": "pr112-run-pr-tests",
    "updated": true
  }
}

Reinforce response /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-reinforce.json

{"result":{"content":[{"type":"text","text":"Reinforced memory 3872378d-8561-4ac4-bc6b-bfacc31d320f by 0.2"}]},"jsonrpc":"2.0","id":7}```

**Delete response** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-delete.json`
```json
{"result":{"content":[{"type":"text","text":"Deleted memory 3872378d-8561-4ac4-bc6b-bfacc31d320f"}]},"jsonrpc":"2.0","id":8}```

**Get-after-delete response** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-after-delete.json`
```json
{"result":{"content":[{"type":"text","text":"Memory 3872378d-8561-4ac4-bc6b-bfacc31d320f not found."}]},"jsonrpc":"2.0","id":9}```

### MCP large content (truncation regression check)
- Stored content length: 20024
- Retrieved payload file (contains full content): /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-big.payload.json
- Store response: /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-store-big.json
- Get response: /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-big.json

### Persistence (volume survives restart)
**Add** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/persist-add.json`
```json
{"id":"dbb0cb4e-adb4-4455-9247-7c9dd6c033fc","primary_sector":"semantic","sectors":["semantic"],"chunks":1}```

**Get after restart** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/persist-get-after-restart.json`
```json
{"id":"dbb0cb4e-adb4-4455-9247-7c9dd6c033fc","content":"persistence-check-content","primary_sector":"semantic","sectors":["semantic"],"tags":["pr112","persist"],"metadata":{"source":"pr112-run-pr-tests"},"created_at":1767676953482,"updated_at":1767676953482,"last_seen_at":1767676953482,"salience":0.4,"decay_lambda":0.005,"version":1,"user_id":"pr112-user"}```

## How To Reproduce

```bash
scripts/run-pr-tests.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants