Repository navigation
Releases: MystenLabs/walrus-memory
Release list
memwal-python@0.2.0
Release memwal Python SDK v0.2.0
memwal-python@0.1.12
Release memwal Python SDK v0.1.12
@mysten-incubation/memwal@0.1.9
0.1.9
Fixed
rememberBulkandrememberBulkAsyncsend an idempotency key derived from the batch (a 30-minute bucket plus each item's namespace and text). A retry inside that window returns the same jobs instead of storing the batch again. An explicitidempotencyKeystill wins. (#1080)listMemories()pages every stored memory without a similarity query. It returns metadata only: no blob download and no decrypt. An optional namespace filter runs on the client, so a filtered page can be empty whilehas_moreis true. (WALM-388)withMemWaldoes not recall or auto-save on a tool-execution step. A later tool step reuses the recall cached for that user message. Auto-save still runs only on a user turn. (#1079)flush()raises when a background auto-save failed, andonSaveErrorruns. The chat turn itself still does not fail. (#1117)withMemWalwarns when recall fails and acceptsonRecallError. (WALM-764)MemWal.createandMemWalManual.createreject a blankaccountId, an empty namespace string, and a private key that is not a 32-byte Ed25519 seed. An omitted namespace still defaults to"default". (#1155, #1200, #1204)recall,recallManual, andrestorereject alimitthat is not a positive integer before signing.recallalso rejects a blank query, a non-finite scoring weight, and a non-finite or negativemaxDistance. A badmaxDistanceused to drop every hit and look like an empty namespace.recallalso rejects amaxTokensthat is not a number (TypeError) or is negative (RangeError). (#1086, #1068, #1227, #1228)- A
maxDistancefilter keeps the relayer's matchingtotal.totalis no longer replaced by the filtered row count, including when every row is dropped. (WALM-840) - When a token budget keeps only high-relevance hits, the lowest-distance hits are kept and the caller's sort order is preserved. (WALM-756)
waitForRememberJobandwaitForRememberJobsdo not sleep past the deadline. If the budget runs out during the sleep, they make one more status read. (#1081)- Package
exportsinclude adefaultcondition and listtypesfirst, so Next.js, Turbopack, and Noderequire(esm)resolve the subpaths. (WALM-721) - A success response with an empty or non-JSON body raises an SDK error that names the HTTP status, instead of a raw
SyntaxError.MemWalManual.restoresends the SEAL session and keeps its deadline armed across the body read. (#1201, #1137) getRememberBulkStatusrejects a job id list that is not an array before any request.MemWalMock.analyzerejects an invalid date, andMemWalMock.listNamespacesrejects a cursor that is not{updated_at, namespace}instead of ending the page early. (#1226, #1229, #1203)createAccount,addDelegateKey,removeDelegateKey, andMemWalManualwork again without a caller-suppliedsuiClient. They looked forSuiClientin@mysten/sui/client, which current@mysten/suino longer exports, and threwSuiClient not found. The default is nowSuiGrpcClientagainst the public fullnode forsuiNetwork, because those fullnodes no longer serve JSON-RPC. An explicitsuiClientstill wins. (WALM-164, #300)remember,rememberAsync, andrememberAndWaitreject a non-string text argument withTypeErrorbefore signing. An object used to be sent astextand the relayer answered 422. (#1058)getRememberStatusandwaitForRememberJobreject a job id outside[A-Za-z0-9_-]+before signing..,..,\, and%2e%2eused to pass a denylist and be rewritten byfetchafter the path was signed. Valid UUID job ids are unchanged. (#1087, #1216)/manualno longer imports@mysten/walrus.getWalrusClient()andwalrusUpload()had no callers, but the dynamic import still pulled the WASM client into Turbopack and Vite. Downloads stay on the HTTP aggregator. (WALM-145, #330)MemWal.recallManual()is typed asRecallManualHitResult(blob id and distance, no text) andMemWalManual.recallManual()asRecallManualMemoryResult(decrypted text).RecallManualResultremains a deprecated alias of those two result types, not a mixed-item array. (WALM-163, #298)MemWalMock.listNamespacesrejects a non-positive or non-integerlimitwithlimit must be positive, matching the relayer.limit: 0used to returnhas_more: trueandnext_cursor: null, so the documented pagination loop never finished, andlimit: -1silently dropped the last namespace. (#1144, #1145)MemWalMock.rememberBulkAsyncrejects an empty or non-array batch with the same error asMemWal. (#1146)MemWalMockhonorsidempotencyKey. A retry of the same text and namespace returns the original job, a different payload throws, andforget/clearrelease the key so the text can be written again. (#1147)MemWalMockscopes an idempotency key to its namespace, like the relayer. (WALM-781)MemWalMock.recall({ sort: "recent" })orders a bounded semantic window (5×limit, at leastlimit, at most 50) by write time before truncating. Relevance order is unchanged. (#1124)- Positional
recall(query, options, namespace)uses that namespace whenoptions.namespaceis omitted, on bothMemWalandMemWalMock. An explicitoptions.namespacestill wins. (#1038) remember(text, { idempotencyKey })andrememberAndWait(text, { timeoutMs }, { idempotencyKey })treat a plain object in the namespace slot as options. A third options argument is merged, and its fields win. That call used to send the object as the namespace, or drop the third argument and mint a second blob. An explicit string namespace still wins. (#1142)getRememberStatus("")returnsnot_foundwithout a request, andwaitForRememberJob("")fails immediately with status 404.GET /api/remember/is not a route; the empty 404 body used to throwSyntaxError, and the wait treated that as transient until timeout. (#1087)waitForRememberJobssettles every slot when the same job id is listed more than once.indexOfplus aSetof ids left the later copies astimeout. (#1121, #1084)rememberBulkAsyncrejects more than 20 items before signing, matching the relayer'sMAX_BULK_ITEMS. (#1104)- Remember-job status reads (
getRememberStatus,waitForRememberJob,getRememberBulkStatus) no longer attachx-seal-session. Status is metadata and does not decrypt. (#1135) - Bulk status includes each job's
namespace.waitForRememberJobs(jobIds)uses that value when the caller did not pass one, instead of the client default. A caller-supplied namespace still wins. (#1136) getPublicKeyHex()and any request afterdestroy()throw, including a read already in flight.getPublicKeyHex()used to resolve to the zeroed key, or to the well-known key of the wiped seed, andremember()still sentGET /versionbefore noticing the client was destroyed. (#1090)normalizeServerUrlno longer warns about plaintext HTTP for IPv6 loopback.URL.hostnamekeeps the brackets on[::1], so the::1exemption never matchedhttp://[::1]:8000. (#1128)remember,rememberBulk,analyze, andembedreject whitespace-only text immediately, with the same messages the relayer returns. A blank body used to be accepted and then sit until the client timed out. (#1129)- A 401
AUTH_REJECTEDnow says a wallet address is not a MemWal account ID. The relayer still returns one 401 for every credential failure, so the message names the mix-up instead of revealing which check failed. (#1132) rememberBulkAndWait/waitForRememberJobsno longer resolve a batch they could not read as if it were still uploading. The status endpoint counts against the delegate-key budget, and a 429 on every poll used to be retried silently until the wait ran out, leaving each item astimeoutwith "polling timed out", even when nothing had been stored. When no poll got through, the wait now makes one confirming read. If that is rate-limited too, it throwsMemWalRateLimited(status: 429,jobIds,retryAfterSeconds). An item still unsettled at the deadline names its last known status, or says no read got through. (WALM-671, #967)- A
rememberAndWait/waitForRememberJobthat runs out of time now raisesMemWalRememberJobTimeout(stillstatus: 504) carryinglastStatusandserverError, the last state the relayer reported. Under upload congestion the relayer keeps a jobrunningfor up to ~25 minutes and says so, but the 504 used to drop that, so a job that was still retrying looked dead and callers dropped their idempotency key to force a new paid write. When no poll got through, the message says the state is unknown instead. (WALM-670, #966) - Grow
waitForRememberJob/waitForRememberJobspoll delay 1.5× from the caller interval (floor 100ms) toward a 5s cap, with an immediate first poll. Backoff is disabled above the cap. (WALM-623) - A 429 during a remember-job wait is reported on the timeout. Retry-After is still clamped to the caller's remaining budget, and that remainder still buys one status read. (WALM-623)
@mysten-incubation/memwal-mcp@0.0.15
0.0.15
Security
-
A project-local
.memwal/credentials.jsonno longer decides where memory goes on presence alone. That file lives inside the repository, so anyone who could commit to a repo — or get a clone opened — could silently repoint the account and relayer every memory written from that directory went to, including from a subfolder, with nothing said and no delete path once written. A project file is now inert until the user approves that exact project path, account, delegate key and relayer withmemwal-mcp approve-project; the approval record is kept in~/.memwal/project-approvals.json, outside the repository, so a repository cannot carry its own approval, and any later change to the destination requires approving again. Until approved the global credentials are used — an unapproved, altered or malformed project file is a fallback, never a failure — and one stderr line names the file that was skipped, the destination it wanted, and the command that approves it.MEMWAL_CREDS_DIRstill overrides both files without approval.memwal_healthnow reportsaccount=besiderelayer=, so the active destination is visible where the user is. (WALM-639) -
MEMWAL_CREDS_DIRmust now be an absolute path outside the current project, and a value that is not is refused with an error naming it rather than followed. The override decides the credentials before any approval is looked up, so it was a way around the gate above rather than through it: an empty value made the approval store the bare relative nameproject-approvals.json, which resolves against the working directory — a repository that committed that one file at its root approved its own credentials — and a relative value such as.memwalput both the credentials and the approval record inside the repository. Neither had to be typed by the user: an MCP client reads.cursor/mcp.json,.vscode/mcp.jsonor.claude/settings.jsonout of the checkout and passes on theenvblock it finds, and editors expand${workspaceFolder}there, so an absolute path inside the project is refused too. An empty value now means unset, matching every other reader of the variable. Tests and CI pointing at an absolute temp directory are unaffected. (WALM-639) -
The login write-ahead record no longer goes into the repository. It sat beside whichever credentials file resolution chose, so once a project file was approved every sign-in wrote a plaintext 64-hex Ed25519 delegate seed to
<repo>/.memwal/login-pending.json— in a directory an attacker who planted the credentials file had already created, so tracked rather than ignored, and staged bygit add -A. It now lives in~/.memwal/login-pending/, keyed by project so a sign-in is still reclaimable only by the project that started it. The credentials file itself cannot move — it is the file the user approved — soapprove-projectand the pre-sign-in warning now state plainly that signing in from this project writes a delegate private key inside the repository, and suggest adding.memwal/to.gitignore. (WALM-639)
Fixed
- The plugin launcher installs
@mysten-incubation/memwal-mcp@0.0.15instead of0.0.14-dev.6. memwal_loginwithfreshKey: truecloses the listener already bound. The replaced listener cannot time out into a failure notice, and a late callback cannot save credentials or clear the new write-ahead record. The tool result names the abandoned public key and address so it can be removed from the dashboard. (WALM-645)
@mysten-incubation/oc-memwal@0.0.7
0.0.7
Fixed
- Only
privateKeyinterpolates${ENV_VAR}. Other fields stay literal so a secret cannot land inserverUrl. memory_searchandmemory_storeare pinned to the calling agent's namespace. An omitted namespace uses that agent, not the main namespace, and a model-supplied namespace is rejected unless it is the agent's namespace or its legacy namespace.
memwal-python@0.1.11
Release memwal Python SDK v0.1.11
@mysten-incubation/memwal@0.1.8
0.1.8
Added
recall()sends its own deadline asdeadline_ms(14000, a second under the 15s it aborts at, so the relayer's answer beats the abort even on a slow connect) in the request body. A relayer that reads it answers a recall about to miss that deadline with a 504 whoseserverCodeisRECALL_TIMEOUTand whose body names the stuck step (auth,embed,vector_search,walrus_download,seal_decrypt), instead of the request aborting with no cause. Relayers that predate the field ignore it. (WALM-396)
Fixed
- Every relayer request now carries a deadline.
fetchhas none of its own and the SDK passed an abort signal on exactly one method (recall, 15s), so the accept POST, every job-status read, and the/versionand/confighandshake calls could stay pending for as long as the socket stayed open. A poll loop checks its budget at the top of each iteration, which bounds when the next request starts rather than how long one takes — so a single stalled read ran straight pasttimeoutMs, and amemwal_rememberdocumented as capping at 90s was observed by an MCP client still running after 120s. Requests default to 30s, matching the relayer's own outbound client; set it withrequestTimeoutMsonMemWal.create, where a non-positive or non-finite value falls back to the default rather than disabling the bound.restore(60s) andanalyze(60s) carry their own, since the route self-bounds at 55s and the extractor LLM runs inline respectively. Inside the wait loops each poll is bounded by the client deadline clamped to the remaining budget, so one stalled poll can neither outlive the wait nor swallow it. An expired request raisesMemWalRequestTimeoutwithstatus: 504, which the existing transient-poll handling already retries; a caller's own abort and every other transport error propagate unchanged. - Generated idempotency keys are derived from the content (a 30-minute bucket plus namespace and text) instead of
crypto.randomUUID(). The per-instance key map only ever deduped retries that reused one client, and callers such as the MCP sidecar build a fresh client per session — so a replay after a reconnect read as a brand-new write and the relayer minted a second paid Walrus blob for one already in flight. The bucket bounds the collapse, sinceremember_jobsrows are never pruned. Callers passing an explicitidempotencyKeyare unaffected, and distinct text or namespaces still derive distinct keys.
@mysten-incubation/memwal-mcp@0.0.14
0.0.14
Unreleased. The plugin's launcher installs @mysten-incubation/memwal-mcp@0.0.14-dev.0, named by mcpPackageVersion in plugin/plugin.json, until 0.0.14 is on npm; drop that key in that release.
Added
- Opt-in Streamable HTTP transport for the stdio bridge. Set
MEMWAL_MCP_TRANSPORT=http(aliasesstreamable,streamable-http) to dial the relayer's single/api/mcpendpoint, where a call is answered on the same request instead of being split across a POST and an SSE stream, so there is no idle watchdog. The default stayssseand unrecognised values fall back to it. Reconnect replay is not transport-aware yet: on Streamable a disconnect mid-send can look sent, so a replayed write may duplicate.
Fixed
- The plugin no longer starts the MCP server through
npx @mysten-incubation/memwal-mcp@<version>. npx resolves a package name against the directory the MCP client was started in — the project the user has open — so a project carrying an installed package of that name, claiming the pinned version, was run instead of the published one; a pinnednpx …@0.0.14command was reproduced running a project's own binary while offline. The version pin was no defence, because the planted package simply claims that version. Every launch site (.mcp.json, the Cursor and Codex copies, and the Codex fallback installer) now runsplugin/scripts/launch_mcp.mjs, which installs the pinned version once under~/.memwal/runtime/memwal-mcp@<version>and launches that absolute entry point with the current node binary. Nothing on that path consults the project'snode_modules, a PATH-relative bin shim, or the project's.npmrc, and the launcher fails instead of falling back to the name when the trusted install cannot be produced. Exploiting the old behaviour required write access to installed package and bin files inside the project, so a plain repository clone or apackage.jsonalone was never enough.MEMWAL_MCP_RUNTIME_DIRrelocates the trusted directory and is rejected unless it is an absolute path. (WALM-640) - The trusted launcher installs
0.0.14-dev.1, notdev.0.dev.0predates the relayer change that returnsmemwal_rememberat accept, so its initializeinstructionsstill call thejob_idreply "the exception, not the normal result" and say ablob_idmeans the fact is stored — the opposite of what the relayer now does on every write. An agent ondev.0against a current relayer therefore reports a fact as saved while the write is still in flight.dev.1carries the corrected text and differs fromdev.0in only auth-required, bridge and instructions. - An unfinished write is no longer reported with a
blob_id.persist_uploaded_statestamps a realblob_idand statusuploadedthe moment the blob is on Walrus, but the job is not done — SetMetadataAndTransfer has not handed the blob object to the user andinsert_vectorhas not indexed it, so the fact is not recallable and a later failure can still lose it.uploadedwas already counted as in flight; the render was not, and printed theblob_idof any row that had one. A live dev batch read0/6 saved, 6 still uploadingwith three of those rows carrying ablob_id=, contradicting the same reply's rule that only ablob_idmeans the fact is stored — an agent that believes it tells the user a fact is saved while it is still in flight. Only adonerow shows ablob_idnow, which is what the single-job path always did. Same guard inmemwal_remember_bulkandmemwal_analyze, whose timeout rows render from the same template. (#921 follow-up) - The cold-start tool list no longer advertises a tool the relayer may not serve. The bridge ships on npm and updates itself while a relayer ships per environment, so 0.0.14-dev.0 dialled prod and staging still on 0.0.13: cold start named
memwal_remember_status, which neither registers, and the pending-write wording sent the agent to go call it — one live run spent 90.67s there before erroring. Cold start is now a floor rather than a forecast (BASELINE_RELAYER_TOOLS): it carries only what the oldest supported relayer serves and its descriptions name nothing outside it, while newer tools still reach the client a beat later on the relayer's owntools/list. Initializeinstructionsuse the same floor (they used to namememwal_remember_statusbefore anytools/list). And a call for a tool outside that floor — including during the cold-start window, before any upstreamtools/listhas been seen — is now answered locally and at once — naming the tools that do exist and saying plainly that nothing ran — instead of being forwarded into a wait that only ends at the orphan deadline. (#928) - A tool call that times out or cannot reach the relayer now says why and what to do, in three lines:
Cause,Relayer health(checked against/healthon the spot) andNext step. It used to readTool error: This operation was aborted. A recall the relayer cut short names the step it was stuck in: credential check, embedding, vector search, Walrus download or SEAL decrypt. Writes are never told a retry is safe. (WALM-396) - A call whose reply never arrives is answered with the relayer's health. The bridge checks
/healthfirst and says whether the relayer is up with this one call stuck, unhealthy, down, or not resolvable at the configured URL, instead of a bare "did not answer". A call being answered this way is never re-sent, so a reconnect landing while the check runs cannot run a write a second time. A lostmemwal_recallreply is now answered after 2 minutes instead of 4. (WALM-396) - Bound every relayer call the tools make. The pinned SDK aborts a request only when the caller passes a signal, which none of the memory methods do, so a stalled socket kept a tool running with no ceiling —
memwal_rememberwas observed still going past 120s against a 90s budget. Accepts are bounded at 15s (MEMWAL_MCP_ACCEPT_DEADLINE_MS), waits at their own budget plus grace. The request is not cancelled — the SDK exposes no way to pass a signal — but the agent is no longer held by it. - Honour the relayer's
retry_afterinstead of dropping the write. Once the per-delegate-key budget (60 weighted requests/minute) is spent the relayer answers 429 with a cooldown, and nothing backed off: the fact was never written and the agent saw only an opaque tool error. A short cooldown is now absorbed; a long one is reported with the wait named, stating plainly that the fact was NOT saved and pointing at the cheaper shape — onememwal_remember_bulkrather than N single calls, onememwal_remember_status(job_ids)rather than N status calls. Only rejections that provably never reached the handler retry, so/api/remember/bulk, which carries no idempotency key, cannot be duplicated by a retry. memwal_remembersends a content-derived idempotency key, so the retry its own timeout message invites really does attach to the job already in flight instead of storing a second paid copy. The key is computed by the tool rather than relied on from the SDK, whose published build mints a random UUID per client instance.memwal_remember_statusacceptsjob_idsto settle a whole batch in one call, and reports a mixed batch honestly — a still-uploading row no longer renders the poll timeout aserror=, which read as a failed write. Settling in one request also matters against the rate limit: 20 ids cost one request, not twenty.- The bridge's cold-start tool list no longer disagrees with the sidecar's.
memwal_remember_statusadvertised onlyjob_id, required, underadditionalProperties: false, so the batch call the tools themselves instruct was rejected untiltools/list_changedarrived; thewaitMsceiling advertised 60000 after the sidecar lowered it to 45000, which came back as an MCP validation error; andmemwal_remember_bulkstill carried its pre-queue description. Tests now pin the parts an agent acts on. - Persist the delegate keypair before sign-in hands the URL to the browser, and reclaim it on the next start. The browser's onchain
add_delegate_keycosts gas and is irreversible, and it happens before the callback that saves the private half, so a client that died in that window destroyed the only copy of a key the user had already paid for and left an orphaned registration nobody could use. (#793) Signing out discards the pending record along with the credentials, a second sign-in against the same relayer reuses the stranded key rather than minting over it, and a sign-in that cannot write the record fails instead of publishing a URL it cannot back. Reclaiming works on Mainnet; Testnet requires an account-id hint the recovering client does not have.
Changed
memwal_remember/memwal_remember_bulkreturn at accept by default (MEMWAL_MCP_REMEMBER_WAIT_MS=0, ~1s,job_id). The Walrus write continues in the background; do not treat that reply as stored. SetMEMWAL_MCP_REMEMBER_WAIT_MS=90000to restore wait-for-blob_id(90s ceiling). Do not use a value between 0 and the real completion time — that pays the wait and still returns pending. The MCP TypeScript SDK's default tools/call timeout is 60s, so a 90s wait loses on hosts that do not raise it.
memwal-python@0.1.10
Release memwal Python SDK v0.1.10
@mysten-incubation/memwal@0.1.7
0.1.7
Added
restore()results includefailed(required liketruncated; SDK defaults omitted to0) for permanent decrypt/UTF-8 failures instead of folding them intoskippedor dropping them silently.
Fixed
- Hash request bodies with
@noble/hashesinstead of WebCrypto-or-node:crypto, so the package no longer imports a Node builtin on a browser-reachable path. Vite externalises such an import without warning: the app builds clean and the browser crashes the first time the path runs. The fallback could never have helped a browser anyway —crypto.subtleis absent precisely when the page is not a secure context, wherenode:cryptois absent too — so it only served Node <19 while being the sole source of the exposure.sha256hexsits on the signed-request path, so every remember and recall reached it. (#322, WALM-136) - Declare
engines.node >= 20.0.0, matchingmemwal-mcpandopenclaw-memory-memwal. The SDK was the only published package without a floor. (WALM-599) - Empty-body 401s now use the same AUTH_REJECTED troubleshooting message as credential 401s instead of telling callers to run
memwal_login. Headless SDK clients do not have that MCP tool. account.tsandmanual.tsPTBs use typedtx.purehelpers instead of the legacy untyped moveCall argument syntax that fails under modern@mysten/sui.