Skip to content

staging <- dev#153

Merged
ducnmm merged 46 commits into
stagingfrom
dev
May 14, 2026
Merged

staging <- dev#153
ducnmm merged 46 commits into
stagingfrom
dev

Conversation

@ducnmm

@ducnmm ducnmm commented May 14, 2026

Copy link
Copy Markdown
Collaborator

staging <- dev

ducnmm and others added 30 commits May 6, 2026 14:20
…ggregator

feat: add seal committee aggregator support
…bility

- packages/mcp: new @memwal/mcp npm package — stdio MCP server, browser
  wallet login flow, auto-reconnect SSE bridge, env presets
  (--prod/--dev/--staging/--local), auth-required fallback that serves a
  friendly inline error when ~/.memwal/credentials.json is missing
- services/server/scripts/mcp: Node sidecar MCP routes (/mcp/sse +
  /mcp/messages), 3s heartbeat keepalive, per-session transport map,
  idempotent cleanup so late close events on replaced transports don't
  wipe the active session
- services/server/src/mcp_proxy.rs: Rust axum reverse proxy with 24h
  timeout override on SSE GET; the shared http_client carried a 30s
  timeout that was killing long tool calls (walrus blob writes take
  25-30s). Adds anti-buffering response headers (X-Accel-Buffering: no,
  cache-control no-transform, connection keep-alive)
- apps/app/src/pages/ConnectMcp: /connect/mcp consent page with sponsored
  add_delegate_key tx + localhost callback. Post-login ClientConfigPanel
  shows paste-ready config snippets for Cursor / Claude Desktop /
  Claude Code / Antigravity with copy buttons and env-aware CLI flags
- services/server/scripts/package.json: bump @mysten/walrus 1.0.3 -> 1.1.7,
  @mysten/sui 2.5.0 -> 2.16.2, @mysten/seal 1.1.0 -> 1.1.3 to match the
  current Walrus contract — older versions hit "balance::destroy_zero"
  abort on remember
- plans: design doc covering Phase A package, Phase B login flow, Phase
  B.5 deferred OAuth-style UX, Phase D dashboard panel
… H4)

C2 — localhost /callback CSRF + DNS rebinding:
- packages/mcp/src/login.ts: generate 32-byte state token, embed in
  connectUrl, require callback to echo it back; constant-time compare
  (timingSafeEqual). Reject if missing or mismatch.
- Validate Host header against {127.0.0.1:port, localhost:port} —
  defeats DNS rebinding to an attacker-controlled name resolving to
  loopback.
- Validate Origin header equals webUrl (no wildcard, no missing).
- Replace `Access-Control-Allow-Origin: *` with exact webUrl. Add `Vary: Origin`.
- Assert `Content-Type: application/json` so simple-request cross-origin
  POSTs (text/plain, form-urlencoded) hit preflight and get blocked
  by the restricted CORS.
- apps/app/src/pages/ConnectMcp.tsx: read `state` from query params,
  validate as 64-hex, include in callback POST payload. Treat missing
  state as invalid query (forces bridge upgrade in lockstep).

H3 — bridge creds-wipe DoS on 401:
- packages/mcp/src/bridge.ts: stop calling clearCreds() on 401. A 401
  from the relayer is evidence of a problem (revoked key, WAF, proxy
  MITM, --local malware) but not proof the saved seed is the cause.
  Auto-wipe turned any one of those into a permanent outage. Now we
  surface a loud error pointing the user at `memwal-mcp login` and
  leave credentials.json untouched.

H4 — --relayer silently rewrote saved creds:
- packages/mcp/src/index.ts: --relayer flag now overrides the relayer
  in-memory for the current process only; the saved credentials.json
  is no longer mutated. Stops malicious config snippets (`--relayer
  https://attacker`) from rewriting saved creds so future spawns ship
  the seed to the attacker. To rotate the saved relayer, the user
  must explicitly `memwal-mcp --logout` then re-login.

Verified locally: SSE handshake unchanged, memwal_remember E2E 27.5s
with blob_id returned, memwal_recall returns saved memories. Build clean.

Remaining audit findings tracked for follow-up:
- C1 (bearer = seed) — needs signed-request migration, larger refactor
- H1, H2, M1-M7, L1-L6 — see plans/2026-05-11-memwal-mcp-package-with-login.md
Resolves conflict in services/server/scripts/sidecar-server.ts (both
sides added a new top-level import on the same line — kept both).

Additional cleanup while resolving:
- services/server/scripts/mcp/index.ts: drop the unused default
  `import express from "express";` runtime binding. Only the type
  imports (Express, Request, Response) are used in this file.

dev brings in:
- seal committee aggregator support (PR #136)
- seal threshold + sidecar config extraction to seal-config.ts
- vite stale-chunk reload fix in apps/app
- doc updates

Verified: packages/mcp tsc clean, apps/app tsc clean, cargo check on
services/server clean. MCP route mount call site unchanged.
CI installed `zod@4.4.3` per the direct dep in sidecar package.json, then
npm rejected the resolution because `@mysten-incubation/memwal@0.0.3`
declares `peerOptional zod@^3.23.0` — incompatible with major 4 even
though `@modelcontextprotocol/sdk@1.29.0` itself accepts `^3.25 || ^4.0`.

Resolved by pinning the sidecar's zod to `^3.25.0` (lockfile picks
3.25.76). Satisfies:
  - @modelcontextprotocol/sdk@1.29.0 → "^3.25 || ^4.0" ✓
  - @mysten-incubation/memwal@0.0.3 → "^3.23.0" ✓

Our zod usage (services/server/scripts/mcp/tools/{remember,recall,
analyze,restore}.ts) is limited to `z.string()`, `.min()`, `.optional()`,
`.describe()` — no zod-v4-only surface — so the downgrade is mechanical.
`plans/2026-05-11-mcp-client-testing-guide.md` and
`plans/2026-05-11-memwal-mcp-package-with-login.md` were committed in
2e788fe by mistake — they are personal/internal design notes, not
intended for the public repo.

Also adds `plans/` to .gitignore so future commits never reach in.

The content is preserved locally in the author's untracked
`.local-plans/` directory.
Step 2 "remember" used to print the HTTP 202 accept envelope and stop:

  { "job_id": "...", "status": "running" }

The relayer continues embedding, encrypting, uploading, and indexing in
the background — but the playground never re-queried so the UI looked
frozen. Users had no way to know whether the memory was actually saved.

Fix: runRemember now runs the accept-then-poll pattern that PR #121
introduced and that the rest of the SDK already follows:

  1. memwal.rememberAsync(text)
     → 202 envelope rendered immediately so the async-job model is still
       visible (the playground's whole point of demonstration).

  2. memwal.waitForRememberJob(job_id, { timeoutMs: 90_000 })
     → signed GET /api/remember/{job_id} polled until terminal. On done,
       the result block updates with { id, blob_id, owner, namespace }
       and the elapsed time. On failed/timeout, the SDK throws with the
       server's error field, surfaced via setRememberError. On timeout
       specifically, the error message includes the job_id so the user
       can curl the status endpoint manually.

Also updates the displayed code snippet in the step header so it
matches the implementation (was a stale single-call example that the
user could copy-paste and hit the same frozen-UI bug).

No SDK / server changes — rememberAsync + waitForRememberJob already
exist in the SDK and the relayer's /api/remember/{job_id} route already
returns the full state machine.
…→ done)

Earlier commit on this branch (df7e019) fixed the frozen-UI bug by
chaining rememberAsync + waitForRememberJob, but waitForRememberJob
only resolves at terminal — the user still didn't see the intermediate
states the relayer's state machine walks through (running → uploaded
→ done). The job felt monolithic.

This commit surfaces each transition.

SDK — packages/sdk/src/memwal.ts:
- Adds a public `getRememberStatus(jobId)` that wraps signed
  `GET /api/remember/{job_id}` and returns the full RememberJobStatus
  (pending | running | uploaded | done | failed | not_found). Mirrors
  the existing public `getRememberBulkStatus` shape. Callers driving
  their own polling loop (UI, dashboards) can now read the live state
  without going through waitForRememberJob (which hides everything
  before terminal).

Playground — apps/app/src/pages/Playground.tsx:
- runRemember rewritten to drive its own polling loop on top of the
  new getRememberStatus. Each poll (1.5s) checks for a status change;
  on transition we push `{status, elapsed_s}` onto a ladder and
  re-render. The result block now shows three sections that update
  live: (1) the 202 accept envelope, (2) the running ladder of state
  transitions with timestamps, (3) the terminal RememberJobStatus
  once `done`/`failed`/`not_found`. Strict 90s overall bound; on
  timeout the error message includes the job_id so the user can curl
  `/api/remember/{job_id}` themselves.
- Imports `RememberJobStatus` type from the SDK.
- Updates the displayed code snippet in the step header to show the
  manual-polling pattern. The earlier snippet referenced
  waitForRememberJob, which is still the right call for production
  code that just wants the final result — the playground deliberately
  picks the more verbose loop so the user can SEE the state machine.

No server changes. /api/remember/{job_id} already returned the full
state machine — the relayer just needed a UI client willing to look
at each value.
Adds `/api/mcp` — a single endpoint that speaks the MCP 2025-06
Streamable HTTP transport. After this lands, users add MemWal to any
MCP client without installing the `@memwal/mcp` package:

    claude mcp add --transport http memwal https://relayer.memwal.ai/api/mcp

Architecture matches MailGate / Linear / Figma's MCP servers — one URL,
session id round-trips through the `mcp-session-id` header, auth on
every request via the existing Bearer scheme (no OAuth dance, no JWT,
no .well-known mess).

Sidecar — services/server/scripts/mcp/index.ts:
- Mounts GET/POST/DELETE /mcp via `StreamableHTTPServerTransport` from
  `@modelcontextprotocol/sdk` 1.29 (already a dep).
- Per-session transport keyed by `mcp-session-id`. The SDK's
  `onsessioninitialized` callback wires the cleanup so a sidecar restart
  doesn't leak transports.
- Auth identical to the existing SSE path — same `resolveAuth(...)` lookup,
  same Bearer + X-MemWal-Account-Id headers. SSE routes
  (/mcp/sse + /mcp/messages) kept alive for the stdio bridge's backward
  compatibility.

Rust relayer — services/server/src/mcp_proxy.rs:
- New `streamable_proxy` handler routed at `/api/mcp` (GET/POST/DELETE/
  OPTIONS). Method dispatched on inbound, body forwarded verbatim,
  response streamed via `Body::from_stream` so both small JSON bodies
  and long-lived SSE upgrades work without buffering.
- Header allowlist extended with `mcp-session-id`, `mcp-protocol-version`,
  `last-event-id` — the SDK on both ends reads these to route
  requests to the right session and resume after disconnects.
- 24h per-request timeout override carried over from the SSE proxy
  fix (commit 8990a88) so walrus blob writes don't trip the shared
  http_client's 30s ceiling.

Dashboard — apps/app/src/pages/ConnectMcp.tsx:
- `ClientConfigPanel` adds a transport-mode pill (`stdio + package` vs
  `http`). Both modes render the JSON snippet inline with copy buttons;
  http mode shows the user's accountId and leaves Bearer as a
  placeholder (the seed lives in ~/.memwal/credentials.json — never
  surfaced to the dashboard for security reasons).
- HTTP hint block tells the user where to pick up the bearer and
  reminds them to treat it like a long-lived API token.

Verified locally (Rust + sidecar both restart cleanly):
- POST /api/mcp initialize → 200, mcp-session-id assigned (~73ms)
- tools/list → 4 tools registered
- tools/call memwal_remember → blob_id returned at T+24.4s end-to-end
  through the new transport

Tradeoff vs full OAuth 2.1: no native "Connect" button in client UIs,
user pastes a 7-line config block manually. Same UX shape as the
stdio package (which also requires a paste). Phase B.5 OAuth remains
on the follow-up list for when we want the Linear/Figma-style
one-click connect; this commit unblocks the Henry-confirmed simplest
path in the meantime.
…session binding

Three reviewer-flagged blockers, plus tests that exercise the fixes
end-to-end via real HTTP.

1. Dockerfile (services/server/Dockerfile)
   sidecar-server.ts now imports ./mcp/index.js, but the runtime stage
   only copied scripts/*.ts. The MCP module was missing in the image,
   so deployed containers would crash at boot with module-not-found.
   Added an explicit COPY scripts/mcp/ ./scripts/mcp/.

2. Rate limit before MCP session creation
   (services/server/scripts/mcp/rateLimit.ts + index.ts)
   resolveAuth() only validates header shape; a flood of forged
   bearers + valid-looking accountIds could open long-lived SSE /
   streamable-HTTP transports and hold sidecar memory + 24h proxy
   streams indefinitely.

   New McpRateLimiter caps:
     - total concurrent sessions globally
     - concurrent active sessions per source IP
     - new session opens per source IP per minute (sliding window)

   Limiter runs BEFORE resolveAuth() on /mcp/sse and on POST /mcp
   for new sessions. Slot is one-shot released on transport close,
   on auth failure, and on handleRequest throw.

   Limits env-tunable: MCP_MAX_TOTAL_SESSIONS,
   MCP_MAX_SESSIONS_PER_IP, MCP_MAX_NEW_SESSIONS_PER_IP_PER_MIN.
   Limiter is lazy-initialized so tests can override env before
   first request (ESM hoists imports above top-level env writes).

   Relayer (services/server/src/mcp_proxy.rs) now injects the real
   client IP via x-forwarded-for on all three MCP routes (sse,
   messages, streamable), preserving any inbound chain. Without
   this the limiter would bucket per loopback.

3. Streamable HTTP session bound to authenticated caller
   (services/server/scripts/mcp/index.ts:handleStreamableHttp)
   Previously, on a request carrying an existing mcp-session-id,
   we dispatched directly without checking the auth matched the
   one that opened the session — anyone who learned the UUID
   could drive an existing transport under their own (or random)
   credentials. Now we reject 403 unless conn.sessionKey ===
   auth.sessionKey (delegate:accountId:delegatePubKey). Mismatch
   logged as session.auth_mismatch.

Tests
-----
services/server/scripts/mcp/__tests__/rateLimit.test.ts (16 unit
tests) — McpRateLimiter caps, releaseFn idempotency, multi-IP
independence, clientIpFromRequest x-forwarded-for parsing,
loadRateLimitConfigFromEnv defaults + garbage handling.

services/server/scripts/mcp/__tests__/integration.test.ts (6 real-
HTTP integration tests) — spins up mountMcpRoutes on an ephemeral
port and verifies via fetch:
  - POST /mcp initialize creates session, returns mcp-session-id
  - 3rd open from same IP within window → 429 ip_burst_cap
  - separate XFF IPs get independent buckets
  - reusing mcp-session-id under different bearer → 403
  - reusing under SAME bearer is accepted (sanity)
  - malformed bearer → 401 with www-authenticate

services/server/src/mcp_proxy.rs (7 cfg(test) tests) —
inject_forwarded_for (empty/existing/whitespace-only inbound,
IPv6 peer), should_forward (allow/block lists),
build_forwarded_headers (drops cookies/host, keeps authorization
+ x-memwal-*).

services/server/scripts/package.json — wires `npm test` to
node --test --import tsx (no new dep; built-in node:test runner).

Verification
------------
  cargo test          → 142 passed (135 prior + 7 new)
  npm test (sidecar)  →  22 passed (16 unit + 6 HTTP integration)
  cargo check         →  clean
…retry classification (MEM-35)

Per Will Bradley (Mysten, 2026-05-12 Slack): coin-object equivocation locks
no longer occur. Collapse the per-wallet Apalis routing layer to a single
wallet, single queue. Classify retry behavior so transient failures get
Apalis backoff and permanent failures (Move abort, object lock) get marked
Dead immediately. Add /metrics/wallet so the simplification can be
validated empirically post-deploy.

Rust changes (services/server/src/):
- types.rs: wallet_storages: Vec<WalletJobStorage> → wallet_storage:
  WalletJobStorage. KeyPool::next_index() always Some(0) when a key is
  configured; extra keys ignored with a warning at boot.
- main.rs: drop per-wallet spawn loop. Single Apalis worker on queue
  'wallet_jobs' with WALLET_JOB_CONCURRENCY env (default 8). Boot logs
  'Multi-wallet routing was retired'.
- routes.rs: enqueue_wallet_job uses single storage; wallet_index in
  payload retained for audit only.
- jobs.rs: WalletJobError::{Transient, Permanent} + classify_sidecar_error
  heuristic on objectlocked/moveabort substrings. Permanent failures emit
  tracing::warn!(target: "wallet_job.permanent") for ops alerting. 3 new
  unit tests cover classification.
- db.rs: wire migration 007.
- migrations/007_collapse_wallet_queues.sql: idempotent rename of
  apalis.jobs.job_type from 'wallet-%' to 'wallet_jobs' for Pending rows.
  Guarded by to_regclass IS NOT NULL so brand-new DBs don't fail.
  Done/Killed rows keep their historical name as audit trail.

Sidecar changes (services/server/scripts/sidecar-server.ts):
- Keep runExclusiveBySigner mutex. Equivocation locks are gone but the
  mutex incidentally fixes Enoki sponsor expiry + SDK default gas-coin
  contention under same-wallet concurrency (testnet measured ~75% failure
  without it at concurrency=4).
- New submitWalletTransaction wrapper around executeWithEnokiSponsor that
  increments counters and classifies errors via regex.
- New GET /metrics/wallet (no auth, before middleware — same posture as
  /health) returning walletSubmittedTotal, walletLockErrorsTotal,
  walletPermanentFailuresTotal, enokiEnabled, suiNetwork.

Local validation:
- cargo test: 139 passed, 0 failed (incl. 3 new classify_* tests).
- tsc --noEmit on sidecar TS: clean.
- Boot smoke with N=5 keys configured: 'Multi-wallet routing was retired'
  fires, single 'wallet_jobs' worker spawned (concurrency=8), migration
  007 applied cleanly.
- Migration 007 idempotency: multiple sequential runs on real Postgres
  exit 0, no row changes.
- End-to-end testnet via UI (apps/app playground): signed remember →
  real Walrus blob created, recall returned stored memories.
- /metrics/wallet after 100+ tx submissions on single wallet:
  walletLockErrorsTotal=0.

Operational impact:
- Wallet count 5 → 1.
- Throughput: 5× parallel → 1× serial via mutex. Acceptable per Daniel's
  'Walrus uploading is happening on the background' (peak < 10 jobs/s).
- Apalis queues: N → 1.

Migration 007 is non-destructive; rollback is git revert + redeploy.
Post-deploy validation: alert on walletLockErrorsTotal > 0 via
/metrics/wallet scrape.
…hertext-in-redis-after-walrus-upload

MEM-37: Cache SEAL ciphertext in Redis
Map permanent wallet failures to terminal Apalis aborts, keep retryable jobs on the collapsed queue during migration, and remove sidecar signer serialization so same-wallet submissions can run concurrently with retries.
Nguyen Mau Minh Duc and others added 14 commits May 13, 2026 10:11
…et-queues

feat(server): collapse multi-wallet Apalis queues to single wallet
… login command (ENG-1749)

Before this change, first-run setup of the stdio MCP package required two
terminal commands:

    codex mcp add memwal -- npx -y @mysten-incubation/memwal-mcp --dev   # register
    npx -y @mysten-incubation/memwal-mcp login --dev                       # browser login

Step 2 forced users to leave their MCP client, run a separate CLI command,
then restart the client. PR #141 review (ducnmm) flagged this as a merge
blocker; Henry confirmed on Slack the same friction applies internally.

After this change, only step 1 is needed. The agent calls memwal_login as
a tool inline; the user clicks the URL in chat, approves the wallet, and
the next memwal_* tool call succeeds.

Implementation
--------------

`packages/mcp/src/auth-required.ts`:
- Advertise a 5th tool `memwal_login` in `tools/list` when no creds exist.
- `tools/call memwal_login` returns the dashboard `/connect/mcp` URL
  near-instantly (~250ms). The HTTP listener stays alive in the background
  for 5 minutes; on user approval, credentials land at
  ~/.memwal/credentials.json. The tool result text repeats the URL three
  times (header, code block, markdown link) so MCP clients that paraphrase
  tool output (Claude Code) cannot strip every copy.
- Pass-through `relayerUrl` / `webUrl` / `label` from the entry point so
  --dev / --staging / --local correctly route to the matching dashboard
  (previously dropped on the auth-required path → always opened prod).
- LOGIN_INSTRUCTION (returned for the other 4 tools when creds missing)
  now leads with "call memwal_login from this client" before the CLI
  fallback.

`packages/mcp/src/bridge.ts`:
- Same `memwal_login` available in BRIDGE mode (when creds exist) so users
  can re-login or switch wallets without removing+re-adding the MCP server.
- New `memwal_logout` tool: clears ~/.memwal/credentials.json and
  instructs the user to revoke the on-chain delegate key from the
  dashboard if they want full revocation. Local-only, never forwarded.
- `tools/list` responses from the relayer get spliced with the two local
  tool definitions on the way back to the client.
- `tools/call memwal_login` / `memwal_logout` intercepted in stdin pump,
  handled locally, never sent to the relayer.

`packages/mcp/src/login.ts`:
- New `onUrl` callback option fired as soon as the localhost listener is
  bound. Lets callers (the tool wrapper) push the URL inline via the MCP
  result before awaiting the callback.

`packages/mcp/src/index.ts`:
- Pass resolved `{relayerUrl, webUrl, label}` through to both
  runAuthRequiredServer and runBridge.

`packages/mcp/package.json`:
- Add `engines: { node: ">=20.0.0" }` so npm install warns on older Node.
- Pin all four runtime deps (drop the `^`). For a public stdio package
  spawned via `npx -y` (always pulls latest), exact pins eliminate the
  supply-chain risk of a transitive dep update introducing a CVE between
  publishes. Lockfile updated accordingly.

Local validation
----------------
- pnpm --filter @mysten-incubation/memwal-mcp build: clean (tsc, no errors)
- Direct stdin smoke against fresh HOME, --dev: tools/list returns 5 tools
  in auth-required mode (with memwal_login); memwal_login tool call
  returns dashboard URL <1s with correct dev.memwal.ai host
- URL routing verified for --prod / --staging / --dev / --local: each
  routes to its matching webUrl + relayerUrl (https://memwal.ai,
  staging.memwal.ai, dev.memwal.ai, localhost:5173)
- openBrowser disabled in MCP context: prevents macOS `open` from
  foregrounding an existing memwal.ai tab instead of navigating to the
  full /connect/mcp?... URL. Users click the URL surfaced in chat instead.

Out of scope (separate ticket)
------------------------------
- Native remote MCP OAuth on /api/mcp HTTP transport (ENG-1750). Covers
  the second surface (HTTP transport), independent code area, larger
  scope. Both flows coexist after both ship.

Closes ENG-1749.
Integrates MEM-35 (multi-wallet → single-wallet collapse) + MEM-37
(SEAL ciphertext Redis cache) + follow-up fixes (single-wallet retry
consistency, concurrent Walrus jobs) from dev.

Conflict in services/server/scripts/sidecar-server.ts:
- HEAD added `mountMcpRoutes(app, ...)` before the shared-secret auth
  middleware (this PR's MCP routes).
- dev added `app.get("/metrics/wallet", ...)` in the same pre-auth
  position (MEM-35 observability).

Both blocks are independent route mounts that need to coexist — kept
both, MCP routes first then /metrics/wallet, both before the
shared-secret check. No semantic overlap; both serve different
endpoints with different trust models.

Verified:
- cargo test: 149 passed, 0 failed (auto-merged main.rs + routes.rs OK)
- pnpm --filter @mysten-incubation/memwal-mcp build: clean
- tsc on sidecar TS: clean
feat(mcp): @memwal/mcp stdio bridge + browser login
…rage + routes split (#147)

Restructures services/server into four focused layers, with zero
behavioural regression against dev:

- src/engine/ — MemoryEngine trait + WalrusSealEngine (production) and
  PlaintextEngine (benchmark). Handlers depend on Arc<dyn MemoryEngine>
  and are mode-blind.
- src/services/ — extracts Embedder, Extractor, LlmChat from routes.rs;
  prompts moved to services/prompts/*.txt versioned text assets bundled
  via include_str!.
- src/storage/ — moves db.rs, seal.rs, sui.rs, walrus.rs from src/ into
  src/storage/.
- src/routes/ — splits the 2,844-line routes.rs into per-endpoint files:
  analyze.rs, recall.rs, remember.rs, admin.rs, sponsor.rs, mod.rs.

Product additions:

- BENCHMARK_MODE=true env flag — selects PlaintextEngine so the AI-
  quality benchmark suite can run without burning Walrus testnet quota.
  Off by default; loud warning on startup. Auth stays mode-blind.
- POST /api/forget — owner-scoped, namespace-wide hard delete on
  vector_entries (used by the benchmark harness for inter-run cleanup).
- POST /api/stats — owner-scoped namespace stats (count + bytes).
- GET /health response gains a "mode" field ("production" | "benchmark")
  — additive, non-breaking.

Rebase absorbed three dev-side feature streams that landed in parallel:

- MEM-35 — single-wallet Apalis queue collapse + WalletJobError retry
  classification (dev commit 316cefd, PR #144).
- MEM-37 — Redis SEAL ciphertext warm cache + BLOB_CACHE_MAX_BYTES size
  cap (dev commit 17fc86b, PR #143). Integrated into WalrusSealEngine
  during conflict resolution so the cap lives next to the cache it
  bounds. Cache policy factored into pure read_decision / write_decision
  helpers for unit-testability.
- MCP server sidecar + proxy routes (PRs #141, #144). Routes mounted in
  main.rs; MEMWAL_RELAYER_URL piped to sidecar.

Migration-number collision resolved by renaming this PR's benchmark-
plaintext migration to 008_*; dev's 007_collapse_wallet_queues.sql is
preserved unchanged.

Defence-in-depth touch-ups (all in scope of the layer split):

- LOW-S1: owner filter on fetch_plaintext_by_blob_id.
- LOW-S5: /api/ask body.limit clamped at 100.
- F3: upfront SEAL credential probe in /api/ask.

Validation:

- cargo test — 159/159 pass (+10 new: MEM-37 cache cap policy, /api/ask
  limit cap, /forget + /stats empty-namespace validation).
- cargo check, cargo fmt --check — clean.
- cargo clippy --all-targets — 8 warnings (carry-over from dev + 1
  expected too_many_arguments on WalrusSealEngine::new after MEM-37
  added blob_cache_max_bytes).
- AI-quality preserved across rebase:
  - LOCOMO three-way (BENCHMARK_MODE=true): 54.5 → 54.8 → 53.5 J
    overall; every category within the ±2-3 J judge-noise floor.
  - LongMemEval (500 oracle queries): 71.7 J overall vs Mem0 49.0 /
    Zep 63.8 / Supermemory 85.4.
- Four-reviewer panel (architect / backend / security / quality)
  signed off MERGE READY.

Benchmark evidence committed at services/server/review/assessment/
benchmark-runs/2026-05-13-eng1747-quality-validation/ (README + 8
result JSONs). auth.rs is a 1-line namespace-rename diff vs dev;
rate_limit.rs is byte-identical.

Closes ENG-1747, MEM-40, MEM-41.
fix(relayer): enable TLS for Neon Postgres
Bring the MCP overview in line with the inline-login flow shipped in
1b296b1. Previously the docs told users to run `npx ... login` from a
separate terminal and restart their MCP client. After ENG-1749 the
stdio package advertises memwal_login and memwal_logout tools, so the
agent can sign in or switch accounts without leaving the chat.

Changes:
- Tools section: split into 4 memory tools + 2 session tools (6 total)
- Streamable HTTP setup: keep the CLI as the way to mint a bearer token
- stdio setup: lead with the inline memwal_login flow; CLI is now a
  fallback. Note the 5-minute URL window and the memwal_logout path
- New "CLI Flags and Environment Variables" section covering --logout,
  --label, --web-url, --login, --help, and MEMWAL_MCP_DEBUG
- Environment presets: surface the dashboard URL alongside the relayer
- Self-hosting: inline the key MCP session-cap env vars instead of
  punting entirely to the env-vars reference

packages/mcp/README.md gets a one-line nudge so the package README also
points users at the inline-login tool rather than implying a manual CLI
step.
docs(mcp): document memwal_login + memwal_logout tools (ENG-1749)
fix(restore): lower default restore limit
Nguyen Mau Minh Duc and others added 2 commits May 14, 2026 15:22
@ducnmm ducnmm temporarily deployed to benchmark-dev May 14, 2026 08:32 — with GitHub Actions Inactive
@railway-app railway-app Bot temporarily deployed to MemWal / dev May 14, 2026 08:32 Inactive
@harrymove-ctrl harrymove-ctrl self-requested a review May 14, 2026 08:56
@ducnmm ducnmm merged commit b49dd9f into staging May 14, 2026
22 of 23 checks passed
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.

5 participants