diff --git a/.github/workflows/benchmark-live.yml b/.github/workflows/benchmark-live.yml index 9c1a214f..4897fc3b 100644 --- a/.github/workflows/benchmark-live.yml +++ b/.github/workflows/benchmark-live.yml @@ -16,7 +16,7 @@ on: - dev - staging server_url: - description: MemWal server URL. Empty uses BENCH_SERVER_URL environment variable. + description: Walrus Memory server URL. Empty uses BENCH_SERVER_URL environment variable. required: false default: '' namespace: diff --git a/.github/workflows/release-oc-memwal.yml b/.github/workflows/release-oc-memwal.yml index 52eed628..c96cbfcf 100644 --- a/.github/workflows/release-oc-memwal.yml +++ b/.github/workflows/release-oc-memwal.yml @@ -1,4 +1,4 @@ -name: Release OC-MemWal Plugin +name: Release Walrus Memory OpenClaw Plugin on: push: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1a69ba5..c881cd6a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: "[QA] Add automated tests to CI (unit + integration + e2e) for MemWal" +name: "[QA] Add automated tests to CI (unit + integration + e2e) for Walrus Memory" on: pull_request: @@ -15,6 +15,23 @@ permissions: contents: read jobs: + compatibility-contract: + name: Compatibility / Contract metadata + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Check compatibility contract + run: node scripts/check-compatibility-contract.mjs + chatbot-e2e: name: Chatbot / Playwright E2E runs-on: ubuntu-latest diff --git a/README.md b/README.md index 119f43ba..08dc2332 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# MemWal +# Walrus Memory Privacy-first AI memory layer for storing encrypted memories on Walrus and retrieving them with semantic search. -> MemWal is currently in beta and actively evolving. While fully usable today, we continue to refine the developer experience and operational guidance. We welcome feedback from early builders as we continue to improve the product. +> Walrus Memory is currently in beta and actively evolving. While fully usable today, we continue to refine the developer experience and operational guidance. We welcome feedback from early builders as we continue to improve the product. ## For AI Agents @@ -30,7 +30,7 @@ import { MemWal } from "@mysten-incubation/memwal"; const memwal = MemWal.create({ key: "your-delegate-key-hex", - accountId: "your-memwal-account-id", + accountId: "your-walrus-memory-account-id", serverUrl: "https://your-relayer-url.com", namespace: "demo", }); @@ -46,7 +46,7 @@ await memwal.restore("demo"); - Full docs at [docs.memwal.ai](https://docs.memwal.ai) - Docs source of truth: `docs/` - Docs site entry points: - - [What is MemWal?](docs/getting-started/what-is-memwal.md) + - [What is Walrus Memory?](docs/getting-started/what-is-memwal.md) - [Quick Start](docs/getting-started/quick-start.md) - [SDK Quick Start](docs/sdk/quick-start.md) - [Relayer Overview](docs/relayer/overview.md) @@ -54,7 +54,7 @@ await memwal.restore("demo"); ## Contributing -We want to be explicit about this while MemWal is in beta: feedback, bug reports, docs fixes, +We want to be explicit about this while Walrus Memory is in beta: feedback, bug reports, docs fixes, examples, and implementation contributions are all welcome. If you spot rough edges or missing guidance, please open an issue or send a PR. @@ -96,7 +96,7 @@ For the full step-by-step setup guide, see: ## OpenClaw / NemoClaw Plugin -[`@mysten-incubation/oc-memwal`](packages/openclaw-memory-memwal) — a memory plugin for [OpenClaw](https://openclaw.ai) agents. It gives OpenClaw persistent, encrypted memory via MemWal with automatic recall and capture hooks. +[`@mysten-incubation/oc-memwal`](packages/openclaw-memory-memwal) — a memory plugin for [OpenClaw](https://openclaw.ai) agents. It gives OpenClaw persistent, encrypted memory via Walrus Memory with automatic recall and capture hooks. ```bash openclaw plugins install @mysten-incubation/oc-memwal diff --git a/SKILL.md b/SKILL.md index 261cbd06..9848bdf4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -2,20 +2,21 @@ name: memwal version: 0.0.1 description: | - Privacy-first AI memory SDK for decentralized storage on Sui blockchain with Walrus. + Walrus Memory SDK for privacy-first AI memory on Sui blockchain with Walrus. Use when users say: - "add memory to my app" - "store encrypted memories" - - "integrate MemWal" + - "integrate Walrus Memory" - "AI agent memory" - "persistent memory SDK" - "Walrus memory storage" - - "setup MemWal" + - "setup Walrus Memory" - "recall memories" keywords: - memwal + - walrus memory - memory sdk - ai memory - encrypted memory @@ -26,15 +27,15 @@ keywords: - vercel ai sdk --- -# MemWal — Privacy-First AI Memory SDK +# Walrus Memory — Privacy-First AI Memory SDK -MemWal is a TypeScript SDK for persistent, encrypted AI memory. It stores memories on Walrus (decentralized storage), encrypts them with SEAL, enforces ownership onchain via Sui smart contracts, and retrieves them with semantic (vector) search. Memories are scoped by `owner + namespace` — each namespace is an isolated memory space. +Walrus Memory is a TypeScript SDK for persistent, encrypted AI memory. It stores memories on Walrus (decentralized storage), encrypts them with SEAL, enforces ownership onchain via Sui smart contracts, and retrieves them with semantic (vector) search. Memories are scoped by `owner + namespace` — each namespace is an isolated memory space. --- ## When to Use -Use MemWal when your app or agent needs: +Use Walrus Memory when your app or agent needs: - **Persistent memory** across sessions, devices, or restarts - **Encrypted storage** — end-to-end encryption, only the owner and authorized delegates can decrypt @@ -48,7 +49,7 @@ Use MemWal when your app or agent needs: ## When NOT to Use - Temporary conversation context that only matters in the current session -- Large file storage (MemWal is optimized for text memories) +- Large file storage (Walrus Memory is optimized for text memories) - Use cases that don't need encryption or decentralization --- @@ -72,7 +73,7 @@ pnpm add @mysten/sui @mysten/seal @mysten/walrus ### 1. Get Your Credentials -You need a **delegate key** (Ed25519 private key) and **account ID** (MemWalAccount object ID on Sui). +You need a **delegate key** (Ed25519 private key) and **account ID** (Walrus Memory account object ID on Sui). Generate them at: - Production: https://memwal.ai or https://memwal.wal.app @@ -84,9 +85,9 @@ Generate them at: import { MemWal } from "@mysten-incubation/memwal"; const memwal = MemWal.create({ - key: "", - accountId: "", - serverUrl: "https://relayer.memwal.ai", + key: process.env.MEMWAL_PRIVATE_KEY!, + accountId: process.env.MEMWAL_ACCOUNT_ID!, + serverUrl: process.env.MEMWAL_SERVER_URL ?? "https://relayer.memwal.ai", namespace: "my-app", }); ``` @@ -94,22 +95,42 @@ const memwal = MemWal.create({ ### 3. Store and Recall Memories ```ts -// Store a memory -const job = await memwal.remember("User prefers dark mode and works in TypeScript."); -await memwal.waitForRememberJob(job.job_id); +// Store one already-distilled fact and wait until it is indexed. +await memwal.rememberAndWait( + "User prefers dark mode and works in TypeScript.", + undefined, + { timeoutMs: 30_000 }, +); // Recall by meaning const result = await memwal.recall("What are the user's preferences?"); console.log(result.results); -// Extract and store facts from text -const analyzed = await memwal.analyze("I live in Hanoi and prefer dark mode."); -await memwal.waitForRememberJobs(analyzed.job_ids); +// Extract facts from free-form text and wait until all accepted facts are indexed. +const analyzed = await memwal.analyzeAndWait( + "I live in Hanoi and prefer dark mode.", + undefined, + { timeoutMs: 30_000 }, +); +console.log(analyzed.facts.map((fact) => fact.text)); // Check relayer health await memwal.health(); ``` +Use `*AndWait` when a workshop UI saves and then immediately recalls in the +same flow. Indexing can lag by a few seconds, so `remember()` / `analyze()` +may return before recall can find the new memory. Manual polling is still +available for advanced async UIs: + +```ts +const accepted = await memwal.remember("User likes Sui."); +const stored = await memwal.waitForRememberJob(accepted.job_id, { + pollIntervalMs: 750, + timeoutMs: 30_000, +}); +``` + --- ## SDK Entry Points @@ -131,7 +152,7 @@ await memwal.health(); |---|---|---| | `remember(text, namespace?)` | Accept one memory job immediately | `{ job_id, status }` | | `rememberAndWait(text, namespace?, opts?)` | Store one memory and wait for completion | `{ id, job_id, blob_id, owner, namespace }` | -| `recall(query, limit?, namespace?)` | Semantic search for memories | `{ results: [{ blob_id, text, distance }], total }` | +| `recall(query, limitOrOptions?, namespace?)` | Semantic search for memories | `{ results: [{ blob_id, text, distance }], total }` | | `analyze(text, namespace?)` | Extract facts and accept one memory job per fact | `{ job_ids, facts, fact_count, status, owner }` | | `analyzeAndWait(text, namespace?, opts?)` | Extract facts and wait for all fact jobs to complete | `{ results, facts, total, succeeded, failed, owner }` | | `restore(namespace, limit?)` | Rebuild missing index entries from Walrus | `{ restored, skipped, total, namespace, owner }` | @@ -146,6 +167,178 @@ await memwal.health(); | `recallManual({ vector, limit?, namespace? })` | Search with pre-computed vector (returns blob IDs only) | | `embed(text)` | Generate embedding vector (no storage) | +### All Response Shapes + +```ts +interface RememberAcceptedResult { + job_id: string; + status: string; +} + +interface RememberJobStatus { + job_id: string; + status: "pending" | "running" | "uploaded" | "done" | "failed" | "not_found"; + owner?: string; + namespace?: string; + blob_id?: string; + error?: string; +} + +interface RememberResult { + id: string; + job_id?: string; + blob_id: string; + owner: string; + namespace: string; +} + +interface RecallMemory { + blob_id: string; + text: string; + distance: number; +} + +interface RecallResult { + results: RecallMemory[]; + total: number; +} + +interface RecallOptions { + limit?: number; + topK?: number; + namespace?: string; + maxDistance?: number; +} + +interface RememberBulkAcceptedResult { + job_ids: string[]; + total: number; + status: string; +} + +interface AnalyzedFact { + text: string; + id: string; + job_id?: string; + blob_id?: string; +} + +interface AnalyzeResult { + job_ids: string[]; + facts: AnalyzedFact[]; + fact_count: number; + status: string; + owner: string; +} + +interface RememberBulkStatusItem { + job_id: string; + status: "pending" | "running" | "uploaded" | "done" | "failed" | "not_found"; + blob_id?: string; + error?: string; +} + +interface RememberBulkStatusResult { + results: RememberBulkStatusItem[]; +} + +interface RememberBulkItemResult { + id: string; + blob_id: string; + status: "done" | "failed" | "timeout"; + namespace: string; + error?: string; +} + +interface RememberBulkResult { + results: RememberBulkItemResult[]; + total: number; + succeeded: number; + failed: number; +} + +interface AnalyzeWaitResult extends RememberBulkResult { + facts: AnalyzedFact[]; + owner: string; +} + +interface EmbedResult { + vector: number[]; +} + +interface RestoreResult { + restored: number; + skipped: number; + total: number; + namespace: string; + owner: string; +} + +interface HealthResult { + status: string; + version: string; + mode?: string; + prompt_versions?: { + extract: string; + ask: string; + }; + relayerVersion?: string; + apiVersion?: string; + minSupportedSdk?: { + typescript: string; + python: string; + mcp: string; + }; + featureFlags?: Record; + deprecations?: Array<{ + surface: string; + deprecatedSince: string; + removalApiVersion: string; + guidance: string; + }>; + build?: { + commit?: string; + buildTimestamp?: string; + }; +} +``` + +`facts[].text` is the extracted fact text to render in UIs. `job_ids[]` +aligns with the accepted fact jobs; use `analyzeAndWait()` when the UI needs +those facts indexed before continuing. + +### Recall Distance and Filtering + +`recall()` returns the closest K memories by vector distance. There is no +default relevance threshold, so small namespaces may return weak filler results +because they are still the closest available matches. + +Lower distance means more similar: + +| Distance | Rough meaning | +|---|---| +| `< 0.25` | Duplicate or very close | +| `0.25 - 0.55` | Related | +| `0.55 - 0.7` | Weak/noisy | +| `>= 0.7` | Usually unrelated | + +Use SDK-side filtering when you only want clearly relevant results: + +```ts +const memories = await memwal.recall("what did I eat yesterday?", { + topK: 10, + namespace: "reading-tracker", + maxDistance: 0.7, +}); +``` + +Equivalent manual filtering: + +```ts +const memories = await memwal.recall("what did I eat yesterday?", 10, "reading-tracker"); +const relevant = memories.results.filter((memory) => memory.distance < 0.7); +``` + --- ## Configuration @@ -155,8 +348,8 @@ await memwal.health(); | Field | Type | Required | Default | Description | |---|---|---|---|---| | `key` | `string` | Yes | — | Ed25519 delegate private key in hex | -| `accountId` | `string` | Yes | — | MemWalAccount object ID on Sui | -| `serverUrl` | `string` | No | `http://localhost:8000` | Relayer URL | +| `accountId` | `string` | Yes | — | Walrus Memory account object ID on Sui | +| `serverUrl` | `string` | No | `https://relayer.memwal.ai` | Relayer URL | | `namespace` | `string` | No | `"default"` | Default namespace for memory isolation | ### Managed Relayer Endpoints @@ -166,6 +359,50 @@ await memwal.health(); | **Production** (mainnet) | `https://relayer.memwal.ai` | | **Staging** (testnet) | `https://relayer.staging.memwal.ai` | +### Framework and Key Handling + +Delegate private keys belong on the server only. In Next.js App Router, call +Walrus Memory from server actions, route handlers, or other server-only modules that +read `MEMWAL_PRIVATE_KEY` from server env. + +`"use server"` files can only export async functions; keep constants, schemas, +and reusable client builders in a separate server-only module. + +```ts +// app/actions/memory.ts +"use server"; + +import { getMemWal } from "@/lib/memwal"; + +export async function savePreference(text: string) { + const memwal = getMemWal(); + return memwal.rememberAndWait(text, "my-app", { timeoutMs: 30_000 }); +} +``` + +```ts +// lib/memwal.ts +import "server-only"; +import { MemWal } from "@mysten-incubation/memwal"; + +export function getMemWal() { + return MemWal.create({ + key: process.env.MEMWAL_PRIVATE_KEY!, + accountId: process.env.MEMWAL_ACCOUNT_ID!, + serverUrl: process.env.MEMWAL_SERVER_URL ?? "https://relayer.memwal.ai", + namespace: "my-app", + }); +} +``` + +Namespace strategy: `owner + namespace` is the isolation boundary. Use one +namespace per app by default, then split by user, team, or feature when a +single app needs separate memory spaces. + +Relayer choice: use staging/testnet for learning and prototypes; use +production/mainnet for production data. Do not mix staging credentials with +mainnet relayer configs. + --- ## Vercel AI SDK Integration @@ -242,9 +479,12 @@ Lifecycle hooks run automatically: |---|---| | `health()` returns error | Check relayer URL is correct and reachable | | `recall()` returns empty | Verify namespace matches what was used in `remember()` | -| `401 Unauthorized` | Verify delegate key is correct and registered on the account | +| `recall()` returns unrelated filler | Recall is top-K without a default relevance threshold; filter by `distance`, for example `distance < 0.7` | +| `401 Unauthorized` | Usually wrong `MEMWAL_PRIVATE_KEY`, key not registered on the account, account ID mismatch, or staging/mainnet mismatch. Check `.env.local` and dashboard credentials | | SDK import errors | Run `pnpm add @mysten-incubation/memwal` — check Node.js ≥ 18 | | Manual client errors | Install peer deps: `@mysten/sui @mysten/seal @mysten/walrus` | +| Direct Sui reads fail or examples look stale | Prefer `SuiGrpcClient` from `@mysten/sui/grpc`; JSON-RPC snippets using `SuiClient` / `getFullnodeUrl` may be stale | +| `forget` expectations are unclear | Current relayer `POST /api/forget` removes vector index rows so memories are unrecallable; Walrus blobs persist until epoch expiry | --- diff --git a/apps/app/.env.example b/apps/app/.env.example index 784c0d30..53cd05bf 100644 --- a/apps/app/.env.example +++ b/apps/app/.env.example @@ -1,5 +1,5 @@ # ══════════════════════════════════════════════════════════════ -# MemWal App — Environment Configuration +# Walrus Memory App — Environment Configuration # ══════════════════════════════════════════════════════════════ # To switch networks: uncomment one block, comment out the other. # ══════════════════════════════════════════════════════════════ @@ -11,7 +11,7 @@ VITE_ENOKI_API_KEY=enoki_public_9aac56cf5c1e5b1254d1fa09bb6e9f0c # Google OAuth Client ID (from Google Cloud Console) VITE_GOOGLE_CLIENT_ID=386692102434-pn0enkrr12r5q3arflsfrrvb14rvhs10.apps.googleusercontent.com -# MemWal Server URL (also handles /sponsor and /sponsor/execute for gasless tx) +# Walrus Memory Server URL (also handles /sponsor and /sponsor/execute for gasless tx) VITE_MEMWAL_SERVER_URL=https://relayer.dev.memwal.ai # Docs URL (separate deployment) diff --git a/apps/app/Dockerfile b/apps/app/Dockerfile index 6b19f37d..24ae9301 100644 --- a/apps/app/Dockerfile +++ b/apps/app/Dockerfile @@ -1,5 +1,5 @@ # ============================================================ -# memwal App — Dockerfile +# Walrus Memory App — Dockerfile # Vite React SPA — build + serve static files # Build context: repo root (Railway Root Directory = /) # ============================================================ diff --git a/apps/app/index.html b/apps/app/index.html index b3716ce9..5434daf8 100644 --- a/apps/app/index.html +++ b/apps/app/index.html @@ -11,22 +11,22 @@ + content="Walrus Memory — privacy-preserving AI memory. Store your AI conversations encrypted on Walrus, searchable with embeddings. You own your data." /> - + - + - MemWal — AI Memory Dashboard + Walrus Memory — AI Memory Dashboard @@ -34,4 +34,4 @@ - \ No newline at end of file + diff --git a/apps/app/public/site.webmanifest b/apps/app/public/site.webmanifest index 693cf762..fec90020 100644 --- a/apps/app/public/site.webmanifest +++ b/apps/app/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "MemWal", - "short_name": "MemWal", + "name": "Walrus Memory", + "short_name": "Walrus Memory", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } @@ -8,4 +8,4 @@ "theme_color": "#000000", "background_color": "#FAF8F5", "display": "standalone" -} \ No newline at end of file +} diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 8934fa6e..41192f32 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -1,5 +1,5 @@ /** - * memwal — Web App + * Walrus Memory — Web App * * Enoki zkLogin integration with @mysten/dapp-kit * Flow: Landing → Sign in with Google (Enoki) → Setup Wizard → Dashboard @@ -49,7 +49,7 @@ interface DelegateKeyState { delegateKey: string | null /** Ed25519 delegate public key (hex) */ delegatePublicKey: string | null - /** Onchain MemWalAccount object ID */ + /** Onchain Walrus Memory account object ID */ accountObjectId: string | null } diff --git a/apps/app/src/pages/ConnectMcp.tsx b/apps/app/src/pages/ConnectMcp.tsx index 51554fac..546e8c7c 100644 --- a/apps/app/src/pages/ConnectMcp.tsx +++ b/apps/app/src/pages/ConnectMcp.tsx @@ -21,7 +21,7 @@ * * Error paths: * - Wallet not connected → wallet picker. - * - User has no MemWalAccount yet → link to /setup. + * - User has no Walrus Memory account yet → link to /setup. * - Wallet rejects tx → retry button. * - localhost callback unreachable → keep success on-chain anyway, ask user * to manually copy creds (rare — only if the MCP listener died). @@ -105,7 +105,7 @@ export default function ConnectMcp() { const port = params.get('port') ?? '' const publicKey = params.get('publicKey') ?? '' const delegateAddress = params.get('delegateAddress') ?? '' - const label = params.get('label') ?? 'MemWal MCP' + const label = params.get('label') ?? 'Walrus Memory MCP' const relayer = params.get('relayer') ?? 'https://relayer.memwal.ai' /** * Cryptographic state token from the MCP bridge. Must be echoed verbatim @@ -166,7 +166,7 @@ export default function ConnectMcp() { setStep('signing') try { - // Resolve the user's MemWalAccount. + // Resolve the user's Walrus Memory account object. const accountId = await resolveAccountId(suiClient, currentAccount.address) if (!accountId) { setStep('no-account') @@ -193,15 +193,15 @@ export default function ConnectMcp() { // Friendly mapping for common contract aborts. if (m.includes('abort code: 0') && m.includes('add_delegate_key')) { setErrorMsg( - `This wallet (${currentAccount.address.slice(0, 10)}…${currentAccount.address.slice(-6)}) is not the owner of MemWalAccount ${accountId.slice(0, 10)}…${accountId.slice(-6)}. ` + - `Switch your wallet to the account that originally created this MemWal, OR run /setup to create a new MemWalAccount for the current wallet.` + `This wallet (${currentAccount.address.slice(0, 10)}…${currentAccount.address.slice(-6)}) is not the owner of Walrus Memory account ${accountId.slice(0, 10)}…${accountId.slice(-6)}. ` + + `Switch your wallet to the account that originally created this Walrus Memory account, OR run /setup to create a new Walrus Memory account for the current wallet.` ) setStep('error') return } if (m.includes('abort code: 2') && m.includes('add_delegate_key')) { setErrorMsg( - `This MemWalAccount already has the maximum number of delegate keys (20). Go to /dashboard and revoke an unused key, then try again.` + `This Walrus Memory account already has the maximum number of delegate keys (20). Go to /dashboard and revoke an unused key, then try again.` ) setStep('error') return @@ -253,7 +253,7 @@ export default function ConnectMcp() {