Repository navigation
Conversation
The TypeScript gap already shipped in 0.1.6. Agent docs still listed the old RecallOptions, which is how GH #708 concluded recency ranking was unreachable.
Agents reading only this file still could not see the weight fields.
…es out (WALM-670)
…ait times out (WALM-670)
Recall is similarity-ranked and limit-bounded, so a write can't be
confirmed and a namespace can't be counted. listMemories() wraps the
existing GET /v1/owners/{owner}/memories and reuses resolveOwner().
Metadata only: no blob fetch, no decrypt.
The relayer has no namespace filter, so the optional namespace is
applied client-side; a filtered page can be empty while has_more is
true. Tombstone fields default for relayers older than WALM-363.
…y reached (WALM-670) A same-key POST on a paid job always answered 202 `uploaded`, even when it lost the recovery claim. And the claim fenced `failed` rows for 60s, so a recovery that had just failed (the wallet job, or an enqueue error that returned before the publish) left a lease behind. The next retry enqueued nothing, still said `uploaded`, and GET kept saying `failed`: the poisoned-key shape for a blob that already exists. The claim now publishes `uploaded` in the same statement. A `failed` row is claimable at once, and only an `uploaded` row keeps the 60s fence. Two racing retries therefore enqueue one recovery, and the loser reports the row's real status. If enqueue fails, the claim is released back to `failed` with the lease cleared, so the next retry takes it immediately.
…d (WALM-670) The timeout message said "retry with the same idempotency key" and that remember_and_wait / rememberAndWait does this for you. Neither is reliably true. `remember` drops its key as soon as the POST is accepted, and an explicit idempotencyKey is never stored, so following the advice minted a new key and a second paid write while the first job was still running. A wait only times out on a job that is still pending, running or uploaded (a failed job raises), so the advice is now to call waitForRememberJob / wait_for_remember_job on the job id again, and to pass an explicit key again on any retry. Python also stores the redacted server_error on the exception attribute, not just in the message.
* fix(sdk): reject bad mock page sizes and honor recent recall MemWalMock.listNamespaces treated limit 0 as an endless page and a negative limit as slice's drop-the-tail. Bulk writes skipped the client's empty-batch check, idempotency keys were ignored, and sort recent stayed oldest-first. Positional recall(query, options, namespace) also dropped the namespace on both the mock and MemWal. Fixes #1144, #1145, #1146, #1147, #1124, #1038 * test(sdk): cover the mock limit, idempotency, and recent-recall edges The first tests matched the issue repros. These also lock the branches a tie-break-only sort, a 50-row window cap, a byte-length key, and a mutated options object would have left green.
…us polls (#1168) * fix(sdk): treat a remember options object as options and fix job status polls remember(text, { idempotencyKey }) and rememberAndWait used to send that object as the namespace. Empty job ids no longer hit GET /api/remember/. Duplicate ids in waitForRememberJobs settle in every slot. Bulk status returns namespace, status polls skip the SEAL session, batches over 20 are rejected before signing, and a destroyed client throws instead of reading a zeroed key or probing /version. #1142 #1087 #1121 #1084 #1104 #1135 #1136 #1090 * fix(sdk): reject a destroyed key still in flight and merge remember options An in-flight getPublicKeyHex resolved to the zeroed cache, and a key derived after destroy was the well-known key of the wiped seed. A third options argument was also dropped when the namespace slot was an object.
…1134) * fix: reject blank text, treat [::1] as local, and pin weekday dates Whitespace-only remember, analyze, and embed text is rejected before a job is created (#1129). The TypeScript client treats bracketed ::1 as loopback (#1128). Fact extraction copies a server-built weekday calendar (#1131). AUTH_REJECTED names a pasted wallet address without changing the 401 (#1132). * refactor: one weekday table, inline the loopback host check The English weekday names lived in a match and again in the calendar loop. One Sunday-first array covers both, indexed by num_days_from_sunday. The IPv6 bracket strip stays inside normalizeServerUrl.
search_similar kept every vector_entries row, so a remember retry or restore could surface the same Walrus blob twice with identical scores.
Duplicate index rows were eating the ANN LIMIT window. Over-fetch by the dropped count, then unique and truncate. Ranker docs no longer narrate WALM-594.
limit + dropped only added the unique shortfall, so a 2x index crawled many HNSW round-trips. Widen by dropped rows in the current window and stop at limit * 4.
…all-returns-duplicate-records-for-the-same fix(server): return each blob once in recall (WALM-594)
…161) recall(query, namespace) was silently treated as options and ran against the default namespace. Fail loudly instead of dropping the intended scope.
recallManual has no object-first overload, so hinting
method({ query, namespace }) sent callers to query.trim() failure.
…itional-overload-silently-accepts-string-as fix(sdk): throw when positional recall gets a namespace string (WALM-161)
…ixes #1143, #1040, WALM-721) - Declare "default" fallback condition across all exported subpaths (".", "./account", "./manual", "./ai") so bundlers (Next.js/Turbopack) and Node 22 require(esm) resolve cleanly instead of throwing ERR_PACKAGE_PATH_NOT_EXPORTED - Place "types" condition first for TypeScript nodenext/bundler resolution - Add exports-map regression tests asserting types-first ordering and default fallback
* fix(mcp): re-check the bridge before signing connect consent (#368) POST /preflight on load already rejects a phishing link. Two pieces of that fix were still missing: a wallet that connects after the consent screen is showing starts add_delegate_key without a click, and the page does not ask the bridge again immediately before the signature. Gate auto-continue on an explicit click, refuse to sign if the bridge no longer confirms the request, and warn when the relayer is not the default. * test(app): a connected wallet does not sign connect consent on its own The consent screen must stay up until the user clicks, even when a wallet is already connected.
…on-to-list-stored-blobs-writes-are-unauditable-and feat(sdk): listMemories() pages every memory without a query (WALM-388)
fix(relayer,sdk): stop a slow remember job reading as a dead key (WALM-670)
… budget (Fixes #1103, WALM-756) - In applyTokenBudget, select candidates by ascending distance under high-relevance-only strategy rather than array order - Preserve the caller's requested order (e.g. sort: recent) for the retained lowest-distance hits - Break distance ties stably by original index - Update TruncationStrategy documentation and recall client commentary - Add regression test reproducing the two-hit recency order bug and multi-hit order preservation
…budget-relevance fix(sdk): retain lowest-distance hits under high-relevance-only token budget (Fixes #1103, WALM-756)
…eck (#1081) - In waitForRememberJob and waitForRememberJobs, clamp the polling sleep delay to Math.max(0, deadline - Date.now()) so the client never sleeps past the wait budget. - Refactor the polling loop so that if the remaining budget expires during sleep, a boundary poll is performed rather than exiting immediately without a status check. - Update pollDeadlineMs to supply a usable minimum timeout (1000ms, clamped to requestTimeoutMs) at or past the boundary so the final check does not immediately abort with a 1ms timeout. - Add regression tests verifying that a job settling before the deadline resolves instead of timing out, and that wait loops do not sleep past the deadline.
…781) - Scope unique partial index on remember_jobs to (owner, namespace, idempotency_key) - Add migration 025 to drop legacy uq_remember_jobs_owner_idempotency_key index - Update conflict handling and idempotency lookups in server routes/remember.rs and routes/analyze.rs - Scope TypeScript MemWalMock idempotency map to namespace + key and document in JSDoc - Add tests covering cross-namespace key re-use and intra-namespace deduplication Fixes WALM-781, #1170
…l, and restore (#1180) * fix(sdk): validate limit is a positive integer in recall, recallManual, and restore - Validate limit is a positive integer before signing requests in recall(), recallManual(), and restore() - Reject limit <= 0, floats, NaN, and invalid types with TypeError - Document relayer cap of 100 on recall and [1, 100] clamp on restore in types JSDoc and api-reference.md - Add comprehensive test coverage in limit-validation.test.mjs Fixes #1086 * fix(sdk): validate limit in MemWalManual.restore recall, recallManual, and MemWal.restore already reject a non-positive or non-integer limit. MemWalManual.restore still forwarded it. Fixes #1086 --------- Co-authored-by: harrymove-ctrl <267878750+harrymove-ctrl@users.noreply.github.com>
#1137) (#1190) * fix(sdk): attach SEAL session and timeout in MemWalManual.restore (Fixes #1137) * test(sdk): drop the unused restore timeout import The tests name the constant in their titles but never reference the binding, which CodeQL flagged on the restore PR. Fixes #1137 * fix(sdk): keep the manual request deadline armed across the body read fetch settles when the headers arrive. Disposing the timer before res.text() or res.json() let a stalled body hang. The deadline now covers that read and is cleared afterwards. Fixes #1137 * fix(sdk): sign manual restore sessions with the delegate key An owner wallet session prompts on every refresh and is broader than the delegate this client already holds. restore() now builds the SEAL session from that delegate key. The timeout test drives restore() and stalls the body after headers. --------- Co-authored-by: harrymove-ctrl <267878750+harrymove-ctrl@users.noreply.github.com>
…llManualHit (#1178) - Add score and created_at fields to RecallMemory dataclass - Add created_at and importance fields to RecallManualHit dataclass - Forward fields from API response in MemWal.recall and MemWal.recall_manual - Populate score and created_at in MemWalMock.recall - Add regression tests in test_client.py and test_mock.py Fixes #1063 Fixes #1160
…#1079) (#1184) * fix(sdk): suppress recall and auto-save on tool execution steps (Fixes #1079) * fix(sdk): keep recalled memory on tool steps without a second recall Auto-save still runs only on a user turn. A later tool step re-injects the cached recall for that same user message, so the answer step does not lose the memory context. Fixes #1079 * fix(sdk): recall when a tool step has no cached memory A matching cache is still reused and does not call recall again. A miss, including the first call on a fresh withMemWal, recalls once and stores that result. Fixes #1079 * fix(sdk): cache an empty recall so tool steps do not call again A user turn with no matching memories used to clear the cache. The next tool step then called /api/recall again. An empty result is now remembered for that turn. Fixes #1079 * fix(sdk): recall again on the next user turn Tool steps still reuse a cached recall, including an empty one. A later user message recalls even when the text matches the previous turn.
Fixes #1107) (#1185) * fix(python-sdk): suppress recall and auto-save on tool execution steps (Fixes #1107) * fix(python-sdk): keep recalled memory on tool steps without a second recall Auto-save still runs only on a user turn. OpenAI and LangChain tool steps re-inject the cached recall for that same user message. Fixes #1107 * fix(python-sdk): treat LangChain generate as one conversation _generate and _agenerate receive one message list, the same list invoke passes. A tool step reuses that turn's cached recall, and a cache miss recalls once instead of dropping the memory. Fixes #1107 * test(python-sdk): call LangChain generate with one conversation The tests added on dev still passed a batch into _agenerate. The wrapper receives one message list, so these calls and their captures match that. * fix(python-sdk): cache an empty recall and refresh on the next user turn Tool steps reuse a recall that matched nothing. A later user message with the same text calls recall again. min_relevance stays 0.3. * fix(python-sdk): sort the same-text recall test imports Ruff I001 failed the Python unit jobs before pytest ran.
* feat(app): restyle the dashboard to the Sept 2026 design Keep delegate keys, namespaces, and the playground, and hide the Walrus Console promo until its calendar date. Console links open console.wal.app, Claude Code install commands match the docs, and pasting a delegate key checks the current account on chain. * fix(app): point the Walrus Console promo at console.walrus.xyz The date gate is unchanged. The links were opening the wrong host. * fix(app): toggle the Walrus Console promo with a boolean flag Replace the calendar-date gate with VITE_WALRUS_CONSOLE_ENABLED. The promo stays off unless that value is true. * feat(app): restyle the MCP connect screens to match the dashboard Use the same dark column, type, and flat cards for the local MCP login and the Claude consent page. The sign-in steps are unchanged. * fix(app): keep the playground open when this browser has no key Stop sending /playground back to the dashboard. Show the same yellow notice with a paste field, and leave Run disabled until a registered delegate key is attached. * fix(app): keep the playground key notice above the steps The shared dashboard alert order was pushing the yellow notice to the bottom of the playground. * fix(app): finish the playground key gate and MCP consent screen Keep the playground open without a key, and restyle the MCP consent page to the same type as the dashboard. Details stay collapsed until opened. * fix(app): open the playground at the top and drop shouted labels Scrolling from the dashboard no longer lands midway down the playground. Buttons and links keep the wording as written instead of full capitals. * fix(app): restore uppercase labels, including the MCP approve button The sentence-case pass was a mistake. Dashboard, playground, and the MCP consent button use the tracked capitals again. * fix(app): bake the Walrus Console flag into the Docker build Railway already sets VITE_WALRUS_CONSOLE_ENABLED, but the image never received it, so the promo stayed off. * fix(app): align the console button with the install steps Put the Walrus Console link in the same column as the step text, and open up the space between the agent steps. * fix(app): apply the dashboard design review notes Keep the install connector on the first tab, show the SDK quickstart in the app step, and drop the AI SDK block. Tighten mobile key rows, selection, and the paste-key form. * fix(app): space the connect cards and center the Codex tab icon Give the subtitle more room above the two connect modules, and keep the Codex mark centered in its tab. * feat(app): restyle the sign-in page and finish the dashboard review Keep the signed-in dashboard and the signed-out sign-in page on one branch. The playground stops after the basic health, remember, and recall steps. * feat(admin): chart memories by day and match the dashboard Add the admin activity API with a 30-day window, keep balance samples long enough for that comparison, and restyle admin plus the sign-in stage to the Sept 2026 layout. * feat(app): send the funnel to Statsig with key join ids Forward trackEvent and page views to Statsig, and attach the delegate public key plus transaction digest on the two successful key events so the visit can be joined to on-chain activity. * fix(docs): exempt the admin activity route from the public reference The freshness check already leaves the other ADMIN_API_KEY routes undocumented. /api/admin/activity belongs in that same list. * fix(app): lower the sign-in layout and match the glass tiles Shift the desktop and mobile content down with the phone, and fill the sign-in buttons white on hover. The right tile is more transparent and the left tile keeps a darker frost. * fix(app): drop the extra background under playground results The result text was sitting in a second gray box inside the yellow frame. Keep the frame and let the JSON sit directly in it. * fix(app): restyle the dashboard confirm dialogs Match the remove-key, delete-memory, and preview dialogs to the dashboard: black panel, Ratch title, and outline pills. * fix(app): use David's mascot-free Open Graph image Point the social preview at the 1200x630 wordmark image and keep the old filename serving the same file so existing links do not 404. * fix(app): replace the sign-in phone with the laptop graphic David's updated sign-in visual uses the dashboard on a laptop instead of a phone. The glass tiles follow that screen, and the top mobile tile sits behind it. * fix(app): enlarge the mobile sign-in laptop and its tiles Give the laptop and glass tiles a little more size, and open up the space above and below them. * fix(app): use the final page title and meta description Kenton confirmed the title and description, including the Open Graph and Twitter tags. * feat(admin): hourly memory chart, click-to-copy addresses, fuller error details Memories written: a 24h window has one calendar day, so the daily chart drew a single bar across the whole card. For windows of 24h or less the activity route now also returns memories_by_hour (same viewer-offset bucketing as the daily query, hours with no jobs filled with zero, absent for longer windows so existing clients are unaffected), and the chart draws one bar per hour. Midnight ticks show the date, and tooltips and the summary name the date and hour. Addresses: the upload-errors table showed only the first six characters (0x07e5). Every admin address (upload errors, top owners, uploader pool, sponsor wallet) now reads 0x5c3a...907e and copies the full value when clicked. A copy icon appears on hover; on click the text turns green, the icon pops to a check and a "Copied" chip rises and fades (red "Copy failed" if the clipboard is refused). The row copy button used to give no feedback at all, since its copied state was only wired to the modal. Error details: the modal showed a timestamp and the message. It now shows the job ID, full owner (copy, and a Suiscan link for the deployment's network), namespace, status, queued and failed times with the gap between them, the message with its own copy, a clear note when the server recorded none, and "Copy all details" as JSON for a ticket. The server already sent status and created_at; the client dropped them. The content area also scrolls now on short screens instead of being cut off. Rust: 2 new unit tests for hourly buckets (positive and negative offsets, empty window); admin_activity 10/10. App: tsc clean, vitest 112/112. * perf(server): index memory_tombstones by deleted_at for admin activity /api/admin/activity counts tombstones in a deleted_at window with no owner, for the current and the previous window. The only index is (owner, deleted_at, memory_id), which cannot serve that range, so each call scanned the table twice, and the admin view polls every 60s per open tab. Adds 024 with CREATE INDEX CONCURRENTLY in its own file (as 016/018/022) and registers it in CONCURRENTLY_BUILT_INDEXES so an interrupted build is recovered. storage::db tests 19/19, including the wiring and registry checks. * fix(app): keep chain ids out of analytics and redact every 64-hex value 7c9dd91 exempted delegate_public_key and transaction_digest from the redaction rules and sent both on delegate_key_register_complete and delegate_key_add_complete to GA/GTM, PostHog and Statsig. - A transaction digest resolves on-chain to the sender's wallet, so every analytics profile (PostHog id, Statsig stableID) became linkable to an address, which the existing redaction policy exists to prevent. - The exemption was keyed by property name, so a real 64-hex private key passed as delegate_public_key was sent unredacted. The old test only used a 63-character value, so it did not catch this. Both fields are removed from the two events and the exemptions are gone: a 64-hex value is redacted under any property name again. The events, page views and the Statsig sink are otherwise unchanged. If joining the funnel to chain activity is still wanted, it needs a privacy sign-off and an id that is not derivable from public chain data. Tests: 64-hex redacted under public-key names; the two key events no longer carry either field. vitest 122/122, tsc -b clean. * fix(app): pin the Statsig script by SRI and keep analytics off /admin The Statsig client is loaded from jsDelivr with no integrity attribute and there is no CSP, while the same origin keeps the delegate private key and the admin API key in sessionStorage. A changed CDN response would run with access to both. - The script tag now carries sha384 integrity for exactly @statsig/js-client@3.33.5/build/statsig-js-client.min.js. The hash was computed locally and matches jsDelivr's published hash for that file (the file is served immutable). In a browser, the correct hash loads and a tampered one is blocked. - No third-party analytics (GTM, PostHog, Statsig) initialises or sends on /admin, which holds the admin key and has nothing to measure. A CSP for the whole app is still worth adding, but it touches GTM, PostHog and wallet popups, so it is left out of this PR. vitest 124/124, tsc -b clean. * fix(app): clear a pasted delegate key once it is imported After "Use this key" succeeded on the dashboard, the pasted private key stayed in plain text in the textarea: its state lives in ConnectWalrusMemory and nothing cleared it (the Playground version does). - importExistingKey now resolves true once the key is stored, and the form clears the field then. A failed import keeps the value so a typo can be fixed. - The field is masked like a password, has autocomplete, autocorrect and autocapitalize off, and carries data-analytics-redact so the click guard treats it as sensitive. Test: the field keeps the key after a failed import and is empty after a successful one. vitest 125/125, tsc -b clean. * fix(app): pin the desktop sign-in laptop to the right edge A wide window letterboxed the device away from the corner. The wordmark follows the stage. * fix(app): match the Back to Console dialog's top padding to its sides The title sat tighter against the top edge than the left and right padding. * fix(app): center the sign-in hero above a 14-inch width The laptop stays on the right edge through 1512px. Wider windows center the group, and the wordmark lines up with the title. * fix(app): put the sign-in wordmark back in the frame's top left The logo sits 50px from the frame edge, left of the Sign in column, as in the design. * feat(app): animate the dashboard copy buttons Copy icons crossfade to a drawn check (or a cross on failure), the wallet address blurs to Copied at a locked width, and the button presses 1px. Copies fall back to execCommand, report failures, and announce the result to screen readers. Motion is CSS-only and off under reduced motion. * fix(app): lay the signed-in pages out for phones Under 640px the Connect Walrus Memory timeline starts flush left on a 48px rail instead of hanging off the first tab, commands scroll sideways instead of breaking mid-word, and the playground title fits. Under 480px the client icons sit above the card title. Under 420px Sign out collapses to its icon so the address no longer runs into the logo. * fix(app): pass VITE_CONSOLE_URL and the legal URLs into the Docker build (#1113) The Dockerfile had no ARG for them, so the Railway values never reached Vite and the Back to Console dialog was compiled out on every deploy. * fix(app): bake the current Console and deployment defaults The Walrus Site build never received the console and legal URLs, and the image still fell back to the pre-cutover packages. Defaults now match the live Railway deployments, and an unset Console URL points at that network's Memory tab. * fix(app): center the sign-in hero on a short 14-inch window A 14-inch MacBook at 100% zoom is shorter than the Figma frame, so pinning the stage right left a wide gap on the left. Center that window. Keep the laptop on the right edge only while the window is at least as tall as a 14-inch fullscreen. * fix(app): grow a short sign-in window until the laptop meets the right edge Use the updated laptop screen and the purple bloom behind it. On a window shorter than a 14-inch display, scale the stage to the full width so the laptop touches the right edge. * fix(app): keep the sign-in size from a 1.9 window and scroll past it Fit the desktop frame to 880px so the empty tail under the laptop tile does not shrink the page. At 1.9:1 and wider the composition stays the size it has at that ratio for the current width, and the page scrolls with the scrollbar hidden. Spare width still leaves 100px of the laptop clipped. --------- Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com> Co-authored-by: Nikola Le <91601109+nikola0x0@users.noreply.github.com>
* docs: add a guide for importing existing agent memory Workshop developers asked for a page that copies local memory, standing context, and session transcripts into Walrus Memory in batches, then keeps using that account. * docs: cover import rate limits, timeouts, and Antigravity Hosted mainnet allows 120 weighted requests per minute and 1000 per hour per account, and 60 per minute per delegate key. A bulk timeout means the upload may still be running, so the guide says to poll those job ids instead of sending the batch again. * docs: drop the staging relayer from the import sample Kimberly confirmed staging stays closed to the public, so the sample no longer points readers at relayer-staging. * docs: make the import sample safe to copy Send Claude memory files through analyze, keep only user and assistant text from a session transcript, and retry a refused call after Retry-After without resubmitting job ids the relayer already accepted. * docs: apply the import-guide review
…1222) /manual still dynamically imported @mysten/walrus for a client nothing called, which pulled the WASM package into Turbopack and Vite. Downloads stay on the HTTP aggregator. MemWal.recallManual returns RecallManualHitResult. MemWalManual.recallManual returns RecallManualMemoryResult. RecallManualResult stays as a deprecated alias of those two, not a mixed list. WALM-145, WALM-163
Keep the legacy (owner, idempotency_key) unique index for this release so a relayer rollback stays safe. The (owner, namespace, idempotency_key) index from 013 and the WALM-781 code still ship; 025 returns in the release after. Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com>
…LM-668) (#1224) testnet-v1.79.0 added `TransactionExpiration::Validity` as variant index 3. The indexer was pinned to testnet-v1.75.1, whose `TransactionExpiration` has three variants, so once testnet began producing `Validity` transactions every checkpoint carrying one failed to convert: transaction.bcs: invalid value: integer 3, expected variant index 0 <= i < 3 The indexer decodes whole checkpoints rather than individual events, so this is a hard stop and not a skipped field: `accounts_v1` retried the same checkpoint indefinitely and its watermark backlog grew to 48,527 pending commits on staging, leaving GET /api/accounts/{owner}/exists answering false for accounts that exist on-chain. Move both Sui pins to testnet-v1.80.0 (ceaaff1c), the current testnet release. The remote store layout is unchanged between the two revisions (same `{checkpoint}.binpb.zst` objects, same zstd + protobuf decode), so this needs no ingestion or deployment configuration change and the saved watermark stays valid. Cover it two ways: `tests/affected_checkpoint.rs` decodes testnet checkpoint 384534623 — the one named in the incident, committed as a fixture in the exact form the remote store serves — through the real `StoreIngestionClient`, and `tests/checkpoint_decoding.rs` pins `Validity` to variant index 3 while holding the earlier variants at their own indices, which already-indexed history depends on. (cherry picked from commit ca3f72d) (cherry picked from commit 69908ac) Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
…#879) * fix(oauth): don't burn the session when Sui is unavailable (WALM-605) `session_complete` consumed the one-time authorization session and only then verified the delegate key on-chain, mapping every `verify_delegate_key_onchain` failure — a Sui `GetObject` 429 included — to `invalid_request("delegate key is not registered on-chain")`. A throttled fullnode therefore looked identical to a genuinely unregistered key, and because the session was already spent the user could not retry: they had to restart `/oauth/authorize` from scratch. Follow-up to WALM-429 (#811), which taught signed HTTP auth the same distinction but left this callback behind. - Classify verify failures with `OnchainVerifyError::is_unavailable()`, the same primitive `auth::cache_reverify_action` uses, so the OAuth callback and signed recall cannot drift apart on what a 429 means. - Move the atomic claim below the on-chain read: only a definitive answer (registered or not registered) spends the session; an unreachable chain leaves the row `pending` so the same request can be re-submitted. Single-use is unchanged — no code is minted until the `WHERE status = 'pending'` update wins, and the session-bound wallet proof still gates everything above it. - Return `temporarily_unavailable` 503 with `Retry-After: 5` instead of a dead-end 400, matching signed auth's `AUTH_UPSTREAM_RETRY_AFTER_SECS`. * review: let the consent UI re-send /complete alone (WALM-605) Sparing the session only helps if something re-POSTs /complete. ConnectClaude did not: any /complete failure went to the terminal error step, and a reload re-ran handleConnect including POST /account, so a first-time connect that had already landed add_delegate_key re-submitted the same pubkey, aborted EDelegateKeyAlreadyExists (code 0), and the SPA reported "this wallet is not the owner". The WALM-605 journey still dead-ended. - fetchJson keeps the HTTP status and the OAuth `error` code. - The /complete request moves behind completeSession(), which holds its payload; on temporarily_unavailable / 503 the page shows a retry step that re-sends that request and nothing else. Definitive rejections still land in the terminal error state. - Tests cover both: the retry path re-POSTs /complete once, issues no second preflight and no second transaction; a 400 stays terminal. - session_complete claims inside its match arms, so the Verified path no longer carries an Option + expect() panic on a request path. - Trim the before/after WALM-605 narration and the test-module essay. * review: survive a reload during OAuth recovery (WALM-605) The retry button worked, but it hung off a useRef. A reload dropped it, sent the user back to consent, and the first-time path then re-ran handleConnect, re-submitted add_delegate_key for a key already on chain, and aborted with code 0 — which the SPA still reports as "this wallet is not the owner". Reloading is the obvious thing to try when a page says "retry", so that was still the reported WALM-605 dead end. - The /complete payload is mirrored into sessionStorage under its session id, so a remount resumes at the retry step and re-sends only /complete. The entry is scoped to the session so one authorization's wallet signature can never be replayed against another, and it is cleared on success. - Auto-connect is suppressed while a recovery payload exists, so the resume cannot fall through into a second /account. - Retry copy no longer claims the key is registered. A 503 means Sui could not be consulted, so it says the authorization is still pending instead. - Tests: a reload resumes and completes with no second /account and no second transaction; a payload from another session is ignored. * review: do not leave a resumable crumb after a spent session (WALM-605) The payload was written to sessionStorage at the start of every `/complete` and cleared only on 200. A definitive 400 (Unregistered has already claimed the row) therefore left it behind, and since `session_view` does not filter on `status = 'pending'`, a reload showed the Sui-busy retry step for a session that was already spent, looping there because nothing ever cleared the key. - The ref is still set up front, so a hung in-flight `/complete` can resume. sessionStorage is written only on the retryable branch, and the terminal branch clears it. - Test: a 400 followed by a remount stays on the error step. - Corrected the comment that claimed the payload joins the existing breadcrumb. It gets its own key precisely because the mount effect rewrites `memwal_claude_connect` to `{ session }` and would drop it. * fix(app): stop mapping add_delegate_key abort 0 to an owner mismatch (WALM-605) `account.move` defines EDelegateKeyAlreadyExists = 0 and ENotOwner = 4, so abort 0 means the key is already registered — not that the wallet is wrong. That is the state a WALM-605 connect lands in: the tx went through and the verify behind it hit a Sui 429, so re-running the flow re-submitted the same pubkey and told the user to switch wallets. Treat abort 0 as already-on-chain and go straight to /complete; report the owner mismatch on abort 4, where it belongs. * review: decide the duplicate-key skip on chain, not from the tx error (WALM-605)`alreadyOnChain` could never fire. ConnectClaude signs through `useSponsoredTransaction`, which throws `new Error(sponsorFailureMessage(...))`, and `sponsor::mask_upstream` collapses every upstream 5xx into a bare 502 `sponsor_upstream_error` that never echoes the upstream body. An Enoki dry-run of a duplicate `add_delegate_key` therefore reaches the SPA as "The transaction was rejected by the sponsor" — no abort code, no function name. The abort 4 and abort 2 matches were dead for the same reason. The test that "covered" this mocked a raw `MoveAbort ... abort code: 0` string, which is not what production throws, so it passed while the user dead-ended.Move the decision to where the answer exists:- `/account` verifies this session's own pending key against the chain and returns `resume_pending_registration`. Verified means an earlier attempt landed the tx, so the UI skips straight to `/complete`. - A Sui outage during that check returns a retryable 503 rather than guessing. Registering walks into the masked 502; skipping sends `/complete` at a key that is not there, which is definitive Unregistered and burns the session. The preflight claims nothing, so a retry costs only that request. - The classification goes through `classify_delegate_verify`, so the preflight and `session_complete` cannot drift on what a Sui 429 means (WALM-429). - `session_account` now sanitises `account_id` before the DB lookup and the on-chain read, matching `session_complete`.Client:- Drop all three abort-code branches. The owner-mismatch guidance they carried was unreachable; `session_complete` still reports a real mismatch as a 400. - Write `PENDING_COMPLETE_KEY` before the request instead of only on a retryable failure. A reload while `/complete` is in flight has no 503 to react to, and that is the one gap the ref cannot cover. Terminal exits still clear it, so a spent session cannot loop on the retry screen. - `tx_digest` is `already-on-chain` when resuming, not `reused-delegate`.Tests:- The sponsor 502 is built from the real `SponsorHttpError` / `sponsorFailureMessage` exports, so the fixture cannot drift from the wrapper, and asserts `/complete` is NOT sent — folding every 502 into "already registered" would push `ENotOwner` / `ETooManyDelegateKeys` into `/complete` and burn the session. - `/account` 503 submits no transaction. - A reload with `/complete` still in flight resumes at the retry step with no second signature and no second preflight. - Four Rust tests pin the preflight decision, including that it and `session_complete` agree on which errors are definitive. * review: return a retryable /account 503 to consent (WALM-605) A preflight POST /account 503 leaves the authorization session pending, but the catch sent it to the terminal error, which has no way back. Return to consent and show the reason so the user can start the request again. * review: use the restyled error class on the retry screens (WALM-605) The rebase onto dev dropped errorTextStyle. Both retry messages now use mcp-error, and the finish button uses mcp-btn. --------- Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
The configuration reference and API reference described key as hex-only and omitted the request deadline.
MemWalMock.analyze accepted an Invalid Date that MemWal throws on. A valid anchor is still not rewritten; the mock has no extractor.
* fix(sdk): reject an invalid recall query, limit, or weight The real client signed a blank query, a NaN limit, and a NaN scoring weight. Those now fail before the request. * test(sdk): expect the positive integer recall limit error dev already rejects limit 0 and below with TypeError. This branch keeps that check.
A string or null jobIds list threw TypeError in the mock. Both clients now throw the same validation error before any request.
NaN and a negative cutoff used to drop every hit and look like an empty namespace.
* fix(sdk): reject an object passed as remember text remember, rememberAsync, and rememberAndWait throw TypeError before signing when text is not a string. * review: reject an object remember text in the mock MemWal throws TypeError. The mock used to fall through to "text cannot be empty".
* fix(sdk): turn an empty or non-JSON 2xx body into an SDK error health() and signed requests called res.json() on success and leaked a raw SyntaxError. Callers now get an error that names the HTTP status. Fixes #1201 * fix(sdk): name empty JSON bodies in MemWalManual and MCP The same 2xx parse guard from MemWal now covers manual restore, compatibility, embed, Walrus upload, and the MCP relayer check. * fix(sdk): give an unreadable 2xx body its HTTP status A remember wait treated a status-less parse error as HTTP 0 and polled until timeout. * review: await the restore body and test the empty JSON path Without await, the deadline disposed before text() finished, so a stalled body became a TypeError. The empty-202 tests now stub the SEAL session and hit /api/remember.
#1235) A cached storage price below the live price makes reserve_space abort in balance::split. The durable path only rebuilt the client on destroy_zero, so every wallet attempt reused the same snapshot.
harrymove-ctrl
approved these changes
Oct 16, 2026
harrymove-ctrl
added a commit
that referenced
this pull request
Oct 18, 2026
* Promote dev → staging (#1182)
* docs: show recall() scoringWeights on SKILL.md (WALM-616)
The TypeScript gap already shipped in 0.1.6. Agent docs still listed the old RecallOptions, which is how GH #708 concluded recency ranking was unreachable.
* docs: define ScoringWeights on SKILL.md (WALM-616)
Agents reading only this file still could not see the weight fields.
* fix(relayer): restart a failed same-key job on the insert-race path too (WALM-670)
* fix(sdk): say what the relayer last reported when a remember wait times out (WALM-670)
* fix(python-sdk): say what the relayer last reported when a remember wait times out (WALM-670)
* fix(python-sdk): say the retry reuses the job only on the same client (WALM-670)
* feat(sdk): listMemories() pages every memory without a query (WALM-388)
Recall is similarity-ranked and limit-bounded, so a write can't be
confirmed and a namespace can't be counted. listMemories() wraps the
existing GET /v1/owners/{owner}/memories and reuses resolveOwner().
Metadata only: no blob fetch, no decrypt.
The relayer has no namespace filter, so the optional namespace is
applied client-side; a filtered page can be empty while has_more is
true. Tombstone fields default for relayers older than WALM-363.
* fix(relayer): report the paid-recovery state a same-key retry actually reached (WALM-670)
A same-key POST on a paid job always answered 202 `uploaded`, even when it
lost the recovery claim. And the claim fenced `failed` rows for 60s, so a
recovery that had just failed (the wallet job, or an enqueue error that
returned before the publish) left a lease behind. The next retry enqueued
nothing, still said `uploaded`, and GET kept saying `failed`: the
poisoned-key shape for a blob that already exists.
The claim now publishes `uploaded` in the same statement. A `failed` row is
claimable at once, and only an `uploaded` row keeps the 60s fence. Two
racing retries therefore enqueue one recovery, and the loser reports the
row's real status. If enqueue fails, the claim is released back to
`failed` with the lease cleared, so the next retry takes it immediately.
* fix(sdk): tell a timed-out remember wait to keep waiting on the job id (WALM-670)
The timeout message said "retry with the same idempotency key" and that
remember_and_wait / rememberAndWait does this for you. Neither is reliably
true. `remember` drops its key as soon as the POST is accepted, and an
explicit idempotencyKey is never stored, so following the advice minted a
new key and a second paid write while the first job was still running.
A wait only times out on a job that is still pending, running or uploaded
(a failed job raises), so the advice is now to call waitForRememberJob /
wait_for_remember_job on the job id again, and to pass an explicit key
again on any retry. Python also stores the redacted server_error on the
exception attribute, not just in the message.
* fix(sdk): reject bad mock page sizes and honor recent recall (#1167)
* fix(sdk): reject bad mock page sizes and honor recent recall
MemWalMock.listNamespaces treated limit 0 as an endless page and a negative
limit as slice's drop-the-tail. Bulk writes skipped the client's empty-batch
check, idempotency keys were ignored, and sort recent stayed oldest-first.
Positional recall(query, options, namespace) also dropped the namespace on
both the mock and MemWal.
Fixes #1144, #1145, #1146, #1147, #1124, #1038
* test(sdk): cover the mock limit, idempotency, and recent-recall edges
The first tests matched the issue repros. These also lock the branches a
tie-break-only sort, a 50-row window cap, a byte-length key, and a mutated
options object would have left green.
* fix(sdk): treat a remember options object as options and fix job status polls (#1168)
* fix(sdk): treat a remember options object as options and fix job status polls
remember(text, { idempotencyKey }) and rememberAndWait used to send that object as the namespace. Empty job ids no longer hit GET /api/remember/. Duplicate ids in waitForRememberJobs settle in every slot. Bulk status returns namespace, status polls skip the SEAL session, batches over 20 are rejected before signing, and a destroyed client throws instead of reading a zeroed key or probing /version.
#1142 #1087 #1121 #1084 #1104 #1135 #1136 #1090
* fix(sdk): reject a destroyed key still in flight and merge remember options
An in-flight getPublicKeyHex resolved to the zeroed cache, and a key
derived after destroy was the well-known key of the wiped seed. A third
options argument was also dropped when the namespace slot was an object.
* fix: reject blank text, treat [::1] as local, and pin weekday dates (#1134)
* fix: reject blank text, treat [::1] as local, and pin weekday dates
Whitespace-only remember, analyze, and embed text is rejected before a job is created (#1129). The TypeScript client treats bracketed ::1 as loopback (#1128). Fact extraction copies a server-built weekday calendar (#1131). AUTH_REJECTED names a pasted wallet address without changing the 401 (#1132).
* refactor: one weekday table, inline the loopback host check
The English weekday names lived in a match and again in the calendar loop. One Sunday-first array covers both, indexed by num_days_from_sunday. The IPv6 bracket strip stays inside normalizeServerUrl.
* fix(server): return each blob once in recall (WALM-594)
search_similar kept every vector_entries row, so a remember retry or restore could surface the same Walrus blob twice with identical scores.
* fix(server): refill recall limit after collapsing duplicate blobs
Duplicate index rows were eating the ANN LIMIT window. Over-fetch by the dropped count, then unique and truncate. Ranker docs no longer narrate WALM-594.
* fix(server): grow recall over-fetch by duplicate factor, cap at 4x
limit + dropped only added the unique shortfall, so a 2x index crawled many HNSW round-trips. Widen by dropped rows in the current window and stop at limit * 4.
* fix(sdk): throw when positional recall gets a namespace string (WALM-161)
recall(query, namespace) was silently treated as options and ran against the default namespace. Fail loudly instead of dropping the intended scope.
* fix(sdk): point recall TypeError at a call shape both APIs share
recallManual has no object-first overload, so hinting
method({ query, namespace }) sent callers to query.trim() failure.
* fix(sdk): add default condition to exports map and place types first (Fixes #1143, #1040, WALM-721)
- Declare "default" fallback condition across all exported subpaths (".", "./account", "./manual", "./ai") so bundlers (Next.js/Turbopack) and Node 22 require(esm) resolve cleanly instead of throwing ERR_PACKAGE_PATH_NOT_EXPORTED
- Place "types" condition first for TypeScript nodenext/bundler resolution
- Add exports-map regression tests asserting types-first ordering and default fallback
* fix(mcp): bind /connect/mcp consent to the local bridge (#368) (#370)
* fix(mcp): re-check the bridge before signing connect consent (#368)
POST /preflight on load already rejects a phishing link. Two pieces of
that fix were still missing: a wallet that connects after the consent
screen is showing starts add_delegate_key without a click, and the page
does not ask the bridge again immediately before the signature. Gate
auto-continue on an explicit click, refuse to sign if the bridge no
longer confirms the request, and warn when the relayer is not the default.
* test(app): a connected wallet does not sign connect consent on its own
The consent screen must stay up until the user clicks, even when a wallet
is already connected.
* fix(sdk): retain lowest-distance hits under high-relevance-only token budget (Fixes #1103, WALM-756)
- In applyTokenBudget, select candidates by ascending distance under high-relevance-only strategy rather than array order
- Preserve the caller's requested order (e.g. sort: recent) for the retained lowest-distance hits
- Break distance ties stably by original index
- Update TruncationStrategy documentation and recall client commentary
- Add regression test reproducing the two-hit recency order bug and multi-hit order preservation
* fix(sdk): clamp polling delay to deadline and ensure boundary poll check (#1081)
- In waitForRememberJob and waitForRememberJobs, clamp the polling sleep delay to Math.max(0, deadline - Date.now()) so the client never sleeps past the wait budget.
- Refactor the polling loop so that if the remaining budget expires during sleep, a boundary poll is performed rather than exiting immediately without a status check.
- Update pollDeadlineMs to supply a usable minimum timeout (1000ms, clamped to requestTimeoutMs) at or past the boundary so the final check does not immediately abort with a 1ms timeout.
- Add regression tests verifying that a job settling before the deadline resolves instead of timing out, and that wait loops do not sleep past the deadline.
* fix(server,sdk): scope remember idempotency keys per namespace (WALM-781)
- Scope unique partial index on remember_jobs to (owner, namespace, idempotency_key)
- Add migration 025 to drop legacy uq_remember_jobs_owner_idempotency_key index
- Update conflict handling and idempotency lookups in server routes/remember.rs and routes/analyze.rs
- Scope TypeScript MemWalMock idempotency map to namespace + key and document in JSDoc
- Add tests covering cross-namespace key re-use and intra-namespace deduplication
Fixes WALM-781, #1170
* fix(sdk): derive and forward idempotencyKey in rememberBulk (Fixes #1080)
* fix(python-sdk): anchor memory system instruction to start of prompt and catch cross-loop ValueError (Fixes #1159, WALM-817)
* fix(python-sdk): support HTTP-date Retry-After and prevent float overflow (Fixes #1151, WALM-815)
* fix(python-sdk): retry transport errors and bound polls by deadline in wait_for_remember_job (Fixes #1000, WALM-696)
* fix(sdk): validate recall maxTokens and preserve matching total (Fixes #1068)
- Add and export validateMaxTokens() to reject negative budgets with RangeError and non-numeric budgets with TypeError before making network requests
- Call validateMaxTokens() early in MemWal.recall(), MemWalMock.recall(), and applyTokenBudget()
- Preserve matching hit count (processed.total / ranked.length) in recall() results instead of overwriting total with budgeted.length (#1068, #1066)
- Add comprehensive unit and e2e tests covering maxTokens validation and total preservation
* fix(sdk): warn on recall failure in withMemWal and add onRecallError callback (Fixes #1118, WALM-764)
* fix(python-sdk): wrap docstring and tighten mock-compat TypeError inspection in poll loop
* fix(python-sdk): fix ruff import sorting/unused imports and document wall clock usage in _http_date_retry_ms
* fix(python-sdk): support LangChain multimodal system message, copy model attributes, and fix ruff line lengths
* fix(sdk): validate bulk idempotencyKey on MemWalMock and skip prefix length check on derived items
* test(python-sdk): freeze time in HTTP-date retry test to eliminate subsecond truncation race
* fix(python-sdk): derive content idempotency key in remember (Fixes #1094, WALM-800)
Derive content-based idempotency keys r1-{sha256(bucket \0 namespace \0 text)}
matching the TypeScript SDK specification (30-minute bucket window) when
idempotency_key is omitted in remember() and remember_and_wait().
Previously, Python fell back to str(uuid.uuid4()) on each completed write
or new client session, causing identical writes to mint fresh paid blobs
rather than collapsing onto the existing job.
* fix(sdk): reject an empty namespace at construction
MemWalMock already throws "namespace cannot be empty". MemWal and
MemWalManual treated "" as a real namespace and sent it on the wire.
Omitted namespace still defaults to "default".
Fixes #1204
* fix(sdk): require a 32-byte Ed25519 seed at construction
hexToBytes accepted any even-length hex, so a 40-byte key or a missing
key survived MemWal.create and failed later inside the signer.
Fixes #1200
* fix(sdk): reject a malformed listNamespaces cursor in the mock
A cursor that JSON-parsed but was not {updated_at, namespace} made
date comparisons NaN and returned an empty page with has_more false,
so a pagination walk stopped while namespaces still existed.
Fixes #1203
* fix(chatbot): rate-limit unauthenticated guest user creation (WALM-415)
GET /api/auth/guest inserted a User row on every call. Throttle before
signIn so one IP cannot farm guest rows in local or production.
* fix(chatbot): consume guest rate limit in authorize and wait for Redis (WALM-415)
Guest inserts go through authorize(), not only GET /api/auth/guest. Await Redis connect before fail-closed 503 so cold starts are not blocked.
* fix(chatbot): reconnect Redis and map guest limiter through Auth.js
A leftover settled connectPromise skipped reconnect after chat warmed
the client. Drain on settle, raise the site-wide guest cap, and surface
authorize() 429/503 as JSON instead of CallbackRouteError 500.
* fix(sdk): default account and manual helpers to a gRPC Sui client (WALM-164)
createAccount, addDelegateKey, removeDelegateKey and MemWalManual looked
for SuiClient in @mysten/sui/client when the caller omitted suiClient.
Current @mysten/sui no longer exports it, so they threw "SuiClient not
found" before doing anything.
Build SuiGrpcClient against the public fullnode for suiNetwork instead.
Those fullnodes no longer serve JSON-RPC, so a JSON-RPC default would
construct and then fail on its first call. An explicit suiClient still
wins.
Fixes #300
* fix(sdk): reject unsafe remember job ids (WALM-833)
* fix(sdk): reject unsafe remember job ids and add Python status
getRememberStatus and waitForRememberJob no longer sign a job id that
is empty or contains a slash, question mark, hash, or whitespace.
Python MemWal and MemWalSync gain get_remember_status, and an empty
404 body is not_found instead of json.JSONDecodeError.
WALM-833, WALM-832, #1100, #1096, #1216
* fix(sdk): allowlist remember job ids and parse status 404s locally
`.` `..` `\` and `%2e%2e` passed the denylist and were rewritten after
signing. Both SDKs now accept only one [A-Za-z0-9_-] segment. A UUID
object still polls; any other non-string is a TypeError. Empty and
non-object 404 bodies are not_found in the status readers, not in the
shared signed request.
* fix(sdk): stop preflights from consuming recall's abort budget (WALM-598) (#883)
* fix(sdk): stop preflights from consuming recall's abort budget (WALM-598)
`recall()` armed a single 15s AbortController before calling
`signedRequest`, which then awaited two unabortable, untimed preflights:
`ensureCompatibleRelayer()` (`GET /version`, falling back to `GET
/health`) and `buildSealSession()` (`GET /config` plus a Sui RPC
round-trip for `SessionKey.create`). Only the final `/api/recall` fetch
received the signal. A relayer whose `/health` p50 had drifted to ~9s
therefore spent most of the caller's budget before recall was dispatched,
and the resulting `AbortError` surfaced on the recall fetch even when
recall itself was healthy (GH #438). Both preflights are memoized, so
this hit the first recall of a process and every recall after the 5-minute
SEAL session expiry.
SDK:
- Move the deadline into `signedRequest`, armed after the preflights
resolve, so it covers only the request it names.
- Give `/version`, `/health` and `/config` their own bounded budget
(`preflightTimeoutMs`, default 5000). They previously carried no signal
at all and could hang indefinitely.
- Make recall's budget configurable via `MemWalConfig.recallTimeoutMs`
and `RecallOptions.timeoutMs`; the default stays 15000.
- Throw a `TimeoutError` whose `phase` names the round-trip that stalled
instead of an unlabeled `AbortError`. Export `isTimeoutError`.
- Bound the standalone `health()` and the manual client's `/version` ->
`/health` fallback the same way.
Server, in support of keeping `/health` a sub-second connectivity check:
- Add single-flight to `sidecar_write_ready`. The 2s TTL was checked
under the lock but not held across the probe, so every request arriving
during an in-flight probe started its own.
- Replace the blocking `std::sync::Mutex` with `tokio::sync::RwLock`.
Besides parking a worker thread on an async path, the mutex could
poison: the read side recovered but the write side was a bare `if let
Ok(..)`, so one panic froze the cached timestamp and every subsequent
`/health` fanned out to the sidecar for the life of the process.
- Instrument the probe with `observe_external` / `record_sidecar_failure`
and raise failure logging from `debug!` to `warn!`. The probe's latency
and failure rate were previously unmeasurable.
The `/health` response shape and its public, unauthenticated status are
unchanged.
* review: hold the deadline through the response body (WALM-598)
fetchWithDeadline cleared its timer as soon as fetch() resolved — which is
when the headers arrive, not the body. signedRequest then ran res.json() /
res.text() with no deadline and no signal, so a server that answers promptly
and trickles the body could outlive the budget the caller named. The old
recall AbortController stayed armed through the body, so this was a
regression in exactly the budget the PR claims to fix.
- The read methods now carry the timer and release it when the body settles.
An abort our own timer caused is still relabelled to a phase-tagged
TimeoutError, so a slow body does not resurface as a bare AbortError.
- The timer is unref'd so a response nobody reads cannot hold the loop open.
- Tests for a trickled body and for a normal read releasing the timer.
- docs/reference/configuration.md now lists recallTimeoutMs,
preflightTimeoutMs and RecallOptions.timeoutMs, so the "full config
surface" pointer in api-reference is true again.
- Shrink the incident recap on the constants, recall(), the preflights and
manual.ts to the contract; fix the admin.rs RwLock rationale, which
claimed the old std::sync::Mutex parked a worker across the probe (it was
dropped before .send().await).
* review: rebase the changelog, make the timer test bite (WALM-598)
- Rebased onto origin/dev. Notes fold into the existing `## 0.1.7` alongside
the landed restore / 401 / tx.pure bullets instead of a separate
`## Unreleased`, and mirror into docs/sdk/changelog.mdx. package.json stays
at origin/dev's 0.1.7; no bump.
- The "releases the timer" test only asserted `res.json()` returned the right
value, which passes whether or not `release()` ran. It now spies the timer
and asserts both halves of the fix: the deadline is still armed when
fetch() resolves, and gone once the body settles.
- Style-guide pass on docs/sdk/api-reference.md:59 and :128.
* fix: drop an accidental packages/sdk/node_modules symlink
A local node_modules symlink was staged with the previous commit. Root
.gitignore lists `node_modules/`, which matches directories only, so a
symlink of that name was never ignored. On any other checkout it is a broken
link pointing at a machine-local absolute path, and pnpm install fails with
ENOENT in packages/sdk.
* fix(sdk): let the body deadline fire on an idle event loop (WALM-598)
The deadline timer was `unref()`d, so once it became the thing that bounds
the body read it could no longer fire when nothing else held the loop open.
Node 22 drained the loop mid-test and cancelled the four body-deadline tests
with 'Promise resolution is still pending but the event loop has already
resolved'; a caller awaiting a trickled body on an otherwise-idle loop hit
the same hole.
Keep the timer referenced. An unread response body already holds a socket
handle open in undici, so unref'ing bought nothing it was supposed to.
* rebase: reconcile WALM-598 budgets with the shipped deadline contract
dev landed WALM-648 (every request deadline, `MemWalRequestTimeout` + status
504 so a stalled poll is retried) and WALM-623 after this branch was cut.
Reconciled rather than re-litigated:
- Preflights and recall keep WALM-598's independent budgets, but run through
the client's own `fetchWithDeadline`, which bounds the body read too.
- `requestTimeoutError` gains `phase` and `timeoutMs`, so the stalled
round-trip is named without renaming the shipped error.
- `deadline_ms` is derived from the budget actually in force, so a caller's
`timeoutMs` moves the relayer's clock with it.
* fix(sdk): opt out of a non-positive recall deadline
Rebased onto current dev. Admin health uses WRITE_READY_CACHE_TTL and
WRITE_READY_PROBE_TIMEOUT. recall({ timeoutMs: 0 }) no longer aborts
or sends deadline_ms: 1, and isTimeoutError matches MemWalRequestTimeout.
* fix(sdk): drop the unused recall timeout alias
CodeQL flagged RECALL_REQUEST_TIMEOUT_MS after recall started using the
caller budget directly.
* fix(sdk): honor a disabled recall deadline and the distance total
A client recallTimeoutMs of 0 now omits deadline_ms, same as a per-call 0. maxDistance reports the filtered hit count again.
---------
Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
* fix(mcp): mint a fresh delegate key on forced login (WALM-645) (#946)
* fix(mcp): mint a fresh delegate key on forced login (WALM-645)
`loginFlow` adopted a still-valid pending record unconditionally, so a
stranded login could never be abandoned. With `/api/whoami` absent from the
default relayer, recovery lands in `unavailable` on every start and keeps the
record for its full 24h TTL, while the browser's `add_delegate_key` aborts
with `EDelegateKeyAlreadyExists`. `login` skips recovery but still reused the
record, so it was not an escape hatch either.
`login` now passes `freshKey`, which mints a new keypair and write-aheads it
over the stuck one. The `unavailable` notice no longer says no action is
needed.
* fix(mcp): name the key a fresh login discards, and reach it from the tool (WALM-645)
`freshKey` dropped the pending record and `savePendingLogin` overwrote it
unconditionally, with no equivalent of `backupIfReplacingAnotherAccount`. That
is the loss the write-ahead record exists to prevent: the discarded record was
the only copy of a key the browser may already have paid to register, and
recovery is skipped on this path, so nothing ever named it. `loginFlow` now
reports the discarded record's public key and delegate address before the
overwrite, so the registration can be found in the dashboard.
`freshKey` was also wired only into the CLI. The `memwal_login` tool path goes
through `startOrReuseLoginFlow` from `auth-required` and `bridge`, neither of
which passed it, so a user inside an MCP host stayed stuck with no escape but a
real terminal. Both now take it from an opt-in `freshKey` tool argument,
defaulting to false so an ordinary login still reuses the pending key.
The `unavailable` notice gave key collision as the reason to remove the key,
which cannot happen to a freshly minted one, and pointed at `memwal-mcp login`
rather than the documented npx form. `LOGIN_RETRY_INSTRUCTION` still offered the
retry only to users who had not approved the wallet step, and the bridge's
failure notice still promised the next start reclaims the key, which it cannot
where the relayer has no `/api/whoami`.
* fix(mcp): reset inflight login on freshKey so forced retry mints a new key
* fix(mcp): close the login listener that freshKey replaces
freshKey minted a new key but left the previous listener bound. Its timeout still reported a failure, and a late callback could save credentials or clear the write-ahead record the new login just wrote. Aborting that listener drops both. The tool result names the abandoned public key and address.
WALM-645
* fix(mcp): drop a freshKey login aborted while it is still minting
A second freshKey aborts the controller before the first flow subscribes.
That flow now stops before it writes the pending key or publishes a URL,
so the caller does not keep a link to a listener that is already closed.
* fix(mcp): do not send a replaced login to the CLI fallback
A superseded sign-in tells the caller to open the newer memwal_login result instead of running a second login that would mint over it.
---------
Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
Co-authored-by: ducnmm <165614309+ducnmm@users.noreply.github.com>
* fix(sdk): surface a failed auto-save from flush (#1209)
* fix(sdk): surface a failed auto-save from flush
Background analyze still does not fail the chat turn. flush() used to
report success anyway. It now raises, and onSaveError / on_save_error
fires when the save fails.
Fixes #1117, #1150
* fix(sdk): isolate a throwing onSaveError from the host process
The callback runs inside the background save. If it throws, log it and
still report the original analyze failure from flush().
Fixes #1117, #1150
* fix(sdk): keep auto-save failures without pinning request frames
The ledger stores a type and a one-line message, not the exception
traceback. The callback runs before the record is stored, so a flush
inside the callback cannot drop it. flush also records a task that
failed or was cancelled.
Fixes #1117, #1150
* fix(sdk): keep an auto-save failure when its callback flushes
The failure is recorded before onSaveError / on_save_error. A flush from that callback does not clear it, and flush_sync does not join its own worker.
* fix(sdk): return from on_save_error before joining workers
A flush started inside the callback no longer drains, joins, or clears
the ledger. The callback depth is updated under the lock so two
overlapping workers cannot join each other or drop an increment.
* fix(sdk): only the callback thread skips a failed auto-save
A flush from on_save_error still returns without clearing the failure.
A flush from any other thread waits and raises, which is the signal
#1117 and #1150 ask for.
* fix(sdk): preserve matching total under recall maxDistance filter (Fixes #1066, WALM-840) (#1198)
* fix(sdk): preserve matching total under recall maxDistance filter (Fixes #1066, WALM-840)
- In TypeScript SDK (memwal.ts), avoid overwriting total with filtered.length when maxDistance is applied, preserving the relayer's matching hit count
- In Python SDK (client.py), preserve total from response data instead of overwriting with len(memories) after max_distance filtering
- Update test suites to assert that total reflects matching memories while results carries post-filter hits, allowing callers to distinguish between an empty namespace and results trimmed by distance cutoff
* test(sdk): expect the relayer total after a maxDistance filter
The live e2e still asserted total equalled the filtered length. A
cutoff that drops rows keeps the page total, including when every
row is dropped.
Fixes #1066
* fix(sdk): keep the mock recall total when maxDistance drops the page
The offline mocks now match the HTTP clients: total is the page length before the client-side cutoff. maxTokens still replaces total with the budgeted length. maxDistance itself is unchanged.
* fix(mcp): derive remember idempotency key from resolved namespace (Fixes #1093, WALM-831) (#1195)
* fix(mcp): validate hex input in hexToBytes and support uppercase 0X (Fixes #1162) (#1174)
* fix(python-sdk): redact IPv6, 0.0.0.0, and localhost subdomains in error messages (Fixes #1161) (#1173)
* fix(python-sdk): support uppercase 0X prefix in normalize_private_key (#1172)
Allow private keys with uppercase 0X prefix to be normalized without failing during hex decoding. Matches TypeScript SDK behavior.
Fixes #1163
* fix(sdk): validate accountId in MemWal.create and MemWalManual.create (Fixes #1155) (#1175)
* fix(sdk): validate limit is a positive integer in recall, recallManual, and restore (#1180)
* fix(sdk): validate limit is a positive integer in recall, recallManual, and restore
- Validate limit is a positive integer before signing requests in recall(), recallManual(), and restore()
- Reject limit <= 0, floats, NaN, and invalid types with TypeError
- Document relayer cap of 100 on recall and [1, 100] clamp on restore in types JSDoc and api-reference.md
- Add comprehensive test coverage in limit-validation.test.mjs
Fixes #1086
* fix(sdk): validate limit in MemWalManual.restore
recall, recallManual, and MemWal.restore already reject a non-positive
or non-integer limit. MemWalManual.restore still forwarded it.
Fixes #1086
---------
Co-authored-by: harrymove-ctrl <267878750+harrymove-ctrl@users.noreply.github.com>
* fix(sdk): attach SEAL session and timeout in MemWalManual.restore (Fixes #1137) (#1190)
* fix(sdk): attach SEAL session and timeout in MemWalManual.restore (Fixes #1137)
* test(sdk): drop the unused restore timeout import
The tests name the constant in their titles but never reference the
binding, which CodeQL flagged on the restore PR.
Fixes #1137
* fix(sdk): keep the manual request deadline armed across the body read
fetch settles when the headers arrive. Disposing the timer before
res.text() or res.json() let a stalled body hang. The deadline now
covers that read and is cleared afterwards.
Fixes #1137
* fix(sdk): sign manual restore sessions with the delegate key
An owner wallet session prompts on every refresh and is broader than
the delegate this client already holds. restore() now builds the SEAL
session from that delegate key. The timeout test drives restore() and
stalls the body after headers.
---------
Co-authored-by: harrymove-ctrl <267878750+harrymove-ctrl@users.noreply.github.com>
* fix(python-sdk): expose created_at and score in RecallMemory and RecallManualHit (#1178)
- Add score and created_at fields to RecallMemory dataclass
- Add created_at and importance fields to RecallManualHit dataclass
- Forward fields from API response in MemWal.recall and MemWal.recall_manual
- Populate score and created_at in MemWalMock.recall
- Add regression tests in test_client.py and test_mock.py
Fixes #1063
Fixes #1160
* fix(sdk): suppress recall and auto-save on tool execution steps (Fixes #1079) (#1184)
* fix(sdk): suppress recall and auto-save on tool execution steps (Fixes #1079)
* fix(sdk): keep recalled memory on tool steps without a second recall
Auto-save still runs only on a user turn. A later tool step re-injects
the cached recall for that same user message, so the answer step does
not lose the memory context.
Fixes #1079
* fix(sdk): recall when a tool step has no cached memory
A matching cache is still reused and does not call recall again. A
miss, including the first call on a fresh withMemWal, recalls once
and stores that result.
Fixes #1079
* fix(sdk): cache an empty recall so tool steps do not call again
A user turn with no matching memories used to clear the cache. The
next tool step then called /api/recall again. An empty result is now
remembered for that turn.
Fixes #1079
* fix(sdk): recall again on the next user turn
Tool steps still reuse a cached recall, including an empty one. A later
user message recalls even when the text matches the previous turn.
* fix(python-sdk): suppress recall and auto-save on tool execution steps (Fixes #1107) (#1185)
* fix(python-sdk): suppress recall and auto-save on tool execution steps (Fixes #1107)
* fix(python-sdk): keep recalled memory on tool steps without a second recall
Auto-save still runs only on a user turn. OpenAI and LangChain tool
steps re-inject the cached recall for that same user message.
Fixes #1107
* fix(python-sdk): treat LangChain generate as one conversation
_generate and _agenerate receive one message list, the same list
invoke passes. A tool step reuses that turn's cached recall, and a
cache miss recalls once instead of dropping the memory.
Fixes #1107
* test(python-sdk): call LangChain generate with one conversation
The tests added on dev still passed a batch into _agenerate. The
wrapper receives one message list, so these calls and their captures
match that.
* fix(python-sdk): cache an empty recall and refresh on the next user turn
Tool steps reuse a recall that matched nothing. A later user message with the same text calls recall again. min_relevance stays 0.3.
* fix(python-sdk): sort the same-text recall test imports
Ruff I001 failed the Python unit jobs before pytest ran.
* feat(app): land the Sept 2026 dashboard branch on dev (#1169)
* feat(app): restyle the dashboard to the Sept 2026 design
Keep delegate keys, namespaces, and the playground, and hide the Walrus Console promo until its calendar date. Console links open console.wal.app, Claude Code install commands match the docs, and pasting a delegate key checks the current account on chain.
* fix(app): point the Walrus Console promo at console.walrus.xyz
The date gate is unchanged. The links were opening the wrong host.
* fix(app): toggle the Walrus Console promo with a boolean flag
Replace the calendar-date gate with VITE_WALRUS_CONSOLE_ENABLED. The promo stays off unless that value is true.
* feat(app): restyle the MCP connect screens to match the dashboard
Use the same dark column, type, and flat cards for the local MCP login and the Claude consent page. The sign-in steps are unchanged.
* fix(app): keep the playground open when this browser has no key
Stop sending /playground back to the dashboard. Show the same yellow notice with a paste field, and leave Run disabled until a registered delegate key is attached.
* fix(app): keep the playground key notice above the steps
The shared dashboard alert order was pushing the yellow notice to the bottom of the playground.
* fix(app): finish the playground key gate and MCP consent screen
Keep the playground open without a key, and restyle the MCP consent page to the same type as the dashboard. Details stay collapsed until opened.
* fix(app): open the playground at the top and drop shouted labels
Scrolling from the dashboard no longer lands midway down the playground. Buttons and links keep the wording as written instead of full capitals.
* fix(app): restore uppercase labels, including the MCP approve button
The sentence-case pass was a mistake. Dashboard, playground, and the MCP consent button use the tracked capitals again.
* fix(app): bake the Walrus Console flag into the Docker build
Railway already sets VITE_WALRUS_CONSOLE_ENABLED, but the image never received it, so the promo stayed off.
* fix(app): align the console button with the install steps
Put the Walrus Console link in the same column as the step text, and open up the space between the agent steps.
* fix(app): apply the dashboard design review notes
Keep the install connector on the first tab, show the SDK quickstart in the app step, and drop the AI SDK block. Tighten mobile key rows, selection, and the paste-key form.
* fix(app): space the connect cards and center the Codex tab icon
Give the subtitle more room above the two connect modules, and keep the Codex mark centered in its tab.
* feat(app): restyle the sign-in page and finish the dashboard review
Keep the signed-in dashboard and the signed-out sign-in page on one branch. The playground stops after the basic health, remember, and recall steps.
* feat(admin): chart memories by day and match the dashboard
Add the admin activity API with a 30-day window, keep balance samples
long enough for that comparison, and restyle admin plus the sign-in
stage to the Sept 2026 layout.
* feat(app): send the funnel to Statsig with key join ids
Forward trackEvent and page views to Statsig, and attach the delegate
public key plus transaction digest on the two successful key events so
the visit can be joined to on-chain activity.
* fix(docs): exempt the admin activity route from the public reference
The freshness check already leaves the other ADMIN_API_KEY routes
undocumented. /api/admin/activity belongs in that same list.
* fix(app): lower the sign-in layout and match the glass tiles
Shift the desktop and mobile content down with the phone, and fill the
sign-in buttons white on hover. The right tile is more transparent and
the left tile keeps a darker frost.
* fix(app): drop the extra background under playground results
The result text was sitting in a second gray box inside the yellow
frame. Keep the frame and let the JSON sit directly in it.
* fix(app): restyle the dashboard confirm dialogs
Match the remove-key, delete-memory, and preview dialogs to the
dashboard: black panel, Ratch title, and outline pills.
* fix(app): use David's mascot-free Open Graph image
Point the social preview at the 1200x630 wordmark image and keep the
old filename serving the same file so existing links do not 404.
* fix(app): replace the sign-in phone with the laptop graphic
David's updated sign-in visual uses the dashboard on a laptop instead of a phone. The glass tiles follow that screen, and the top mobile tile sits behind it.
* fix(app): enlarge the mobile sign-in laptop and its tiles
Give the laptop and glass tiles a little more size, and open up the space above and below them.
* fix(app): use the final page title and meta description
Kenton confirmed the title and description, including the Open Graph and Twitter tags.
* feat(admin): hourly memory chart, click-to-copy addresses, fuller error details
Memories written: a 24h window has one calendar day, so the daily chart
drew a single bar across the whole card. For windows of 24h or less the
activity route now also returns memories_by_hour (same viewer-offset
bucketing as the daily query, hours with no jobs filled with zero, absent
for longer windows so existing clients are unaffected), and the chart draws
one bar per hour. Midnight ticks show the date, and tooltips and the
summary name the date and hour.
Addresses: the upload-errors table showed only the first six characters
(0x07e5). Every admin address (upload errors, top owners, uploader pool,
sponsor wallet) now reads 0x5c3a...907e and copies the full value when
clicked. A copy icon appears on hover; on click the text turns green, the
icon pops to a check and a "Copied" chip rises and fades (red "Copy failed"
if the clipboard is refused). The row copy button used to give no feedback
at all, since its copied state was only wired to the modal.
Error details: the modal showed a timestamp and the message. It now shows
the job ID, full owner (copy, and a Suiscan link for the deployment's
network), namespace, status, queued and failed times with the gap between
them, the message with its own copy, a clear note when the server recorded
none, and "Copy all details" as JSON for a ticket. The server already sent
status and created_at; the client dropped them. The content area also
scrolls now on short screens instead of being cut off.
Rust: 2 new unit tests for hourly buckets (positive and negative offsets,
empty window); admin_activity 10/10. App: tsc clean, vitest 112/112.
* perf(server): index memory_tombstones by deleted_at for admin activity
/api/admin/activity counts tombstones in a deleted_at window with no owner,
for the current and the previous window. The only index is
(owner, deleted_at, memory_id), which cannot serve that range, so each call
scanned the table twice, and the admin view polls every 60s per open tab.
Adds 024 with CREATE INDEX CONCURRENTLY in its own file (as 016/018/022)
and registers it in CONCURRENTLY_BUILT_INDEXES so an interrupted build is
recovered. storage::db tests 19/19, including the wiring and registry
checks.
* fix(app): keep chain ids out of analytics and redact every 64-hex value
7c9dd917 exempted delegate_public_key and transaction_digest from the
redaction rules and sent both on delegate_key_register_complete and
delegate_key_add_complete to GA/GTM, PostHog and Statsig.
- A transaction digest resolves on-chain to the sender's wallet, so every
analytics profile (PostHog id, Statsig stableID) became linkable to an
address, which the existing redaction policy exists to prevent.
- The exemption was keyed by property name, so a real 64-hex private key
passed as delegate_public_key was sent unredacted. The old test only
used a 63-character value, so it did not catch this.
Both fields are removed from the two events and the exemptions are gone:
a 64-hex value is redacted under any property name again. The events,
page views and the Statsig sink are otherwise unchanged. If joining the
funnel to chain activity is still wanted, it needs a privacy sign-off and
an id that is not derivable from public chain data.
Tests: 64-hex redacted under public-key names; the two key events no
longer carry either field. vitest 122/122, tsc -b clean.
* fix(app): pin the Statsig script by SRI and keep analytics off /admin
The Statsig client is loaded from jsDelivr with no integrity attribute and
there is no CSP, while the same origin keeps the delegate private key and
the admin API key in sessionStorage. A changed CDN response would run with
access to both.
- The script tag now carries sha384 integrity for exactly
@statsig/js-client@3.33.5/build/statsig-js-client.min.js. The hash was
computed locally and matches jsDelivr's published hash for that file
(the file is served immutable). In a browser, the correct hash loads and
a tampered one is blocked.
- No third-party analytics (GTM, PostHog, Statsig) initialises or sends on
/admin, which holds the admin key and has nothing to measure.
A CSP for the whole app is still worth adding, but it touches GTM, PostHog
and wallet popups, so it is left out of this PR.
vitest 124/124, tsc -b clean.
* fix(app): clear a pasted delegate key once it is imported
After "Use this key" succeeded on the dashboard, the pasted private key
stayed in plain text in the textarea: its state lives in
ConnectWalrusMemory and nothing cleared it (the Playground version does).
- importExistingKey now resolves true once the key is stored, and the form
clears the field then. A failed import keeps the value so a typo can be
fixed.
- The field is masked like a password, has autocomplete, autocorrect and
autocapitalize off, and carries data-analytics-redact so the click guard
treats it as sensitive.
Test: the field keeps the key after a failed import and is empty after a
successful one. vitest 125/125, tsc -b clean.
* fix(app): pin the desktop sign-in laptop to the right edge
A wide window letterboxed the device away from the corner. The wordmark follows the stage.
* fix(app): match the Back to Console dialog's top padding to its sides
The title sat tighter against the top edge than the left and right padding.
* fix(app): center the sign-in hero above a 14-inch width
The laptop stays on the right edge through 1512px. Wider windows center the group, and the wordmark lines up with the title.
* fix(app): put the sign-in wordmark back in the frame's top left
The logo sits 50px from the frame edge, left of the Sign in column, as in the design.
* feat(app): animate the dashboard copy buttons
Copy icons crossfade to a drawn check (or a cross on failure), the wallet
address blurs to Copied at a locked width, and the button presses 1px.
Copies fall back to execCommand, report failures, and announce the result
to screen readers. Motion is CSS-only and off under reduced motion.
* fix(app): lay the signed-in pages out for phones
Under 640px the Connect Walrus Memory timeline starts flush left on a
48px rail instead of hanging off the first tab, commands scroll sideways
instead of breaking mid-word, and the playground title fits. Under 480px
the client icons sit above the card title. Under 420px Sign out collapses
to its icon so the address no longer runs into the logo.
* fix(app): pass VITE_CONSOLE_URL and the legal URLs into the Docker build (#1113)
The Dockerfile had no ARG for them, so the Railway values never reached
Vite and the Back to Console dialog was compiled out on every deploy.
* fix(app): bake the current Console and deployment defaults
The Walrus Site build never received the console and legal URLs, and
the image still fell back to the pre-cutover packages. Defaults now
match the live Railway deployments, and an unset Console URL points at
that network's Memory tab.
* fix(app): center the sign-in hero on a short 14-inch window
A 14-inch MacBook at 100% zoom is shorter than the Figma frame, so
pinning the stage right left a wide gap on the left. Center that
window. Keep the laptop on the right edge only while the window is
at least as tall as a 14-inch fullscreen.
* fix(app): grow a short sign-in window until the laptop meets the right edge
Use the updated laptop screen and the purple bloom behind it. On a window
shorter than a 14-inch display, scale the stage to the full width so the
laptop touches the right edge.
* fix(app): keep the sign-in size from a 1.9 window and scroll past it
Fit the desktop frame to 880px so the empty tail under the laptop tile
does not shrink the page. At 1.9:1 and wider the composition stays the
size it has at that ratio for the current width, and the page scrolls
with the scrollbar hidden. Spare width still leaves 100px of the laptop
clipped.
---------
Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
Co-authored-by: Nikola Le <91601109+nikola0x0@users.noreply.github.com>
* docs: add a guide for importing existing agent memory (#1197)
* docs: add a guide for importing existing agent memory
Workshop developers asked for a page that copies local memory, standing context, and session transcripts into Walrus Memory in batches, then keeps using that account.
* docs: cover import rate limits, timeouts, and Antigravity
Hosted mainnet allows 120 weighted requests per minute and 1000 per hour per account, and 60 per minute per delegate key. A bulk timeout means the upload may still be running, so the guide says to poll those job ids instead of sending the batch again.
* docs: drop the staging relayer from the import sample
Kimberly confirmed staging stays closed to the public, so the sample no longer points readers at relayer-staging.
* docs: make the import sample safe to copy
Send Claude memory files through analyze, keep only user and assistant
text from a session transcript, and retry a refused call after Retry-After
without resubmitting job ids the relayer already accepted.
* docs: apply the import-guide review
* fix(sdk): drop the dead Walrus import and split recallManual results (#1222)
/manual still dynamically imported @mysten/walrus for a client nothing called, which pulled the WASM package into Turbopack and Vite. Downloads stay on the HTTP aggregator.
MemWal.recallManual returns RecallManualHitResult. MemWalManual.recallManual returns RecallManualMemoryResult. RecallManualResult stays as a deprecated alias of those two, not a mixed list.
WALM-145, WALM-163
* fix(server): hold back migration 025 until the next release (#1225)
Keep the legacy (owner, idempotency_key) unique index for this release so
a relayer rollback stays safe. The (owner, namespace, idempotency_key)
index from 013 and the WALM-781 code still ship; 025 returns in the
release after.
Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com>
* fix(indexer): decode checkpoints carrying the Validity expiration (WALM-668) (#1224)
testnet-v1.79.0 added `TransactionExpiration::Validity` as variant index 3. The
indexer was pinned to testnet-v1.75.1, whose `TransactionExpiration` has three
variants, so once testnet began producing `Validity` transactions every
checkpoint carrying one failed to convert:
transaction.bcs: invalid value: integer 3, expected variant index 0 <= i < 3
The indexer decodes whole checkpoints rather than individual events, so this is
a hard stop and not a skipped field: `accounts_v1` retried the same checkpoint
indefinitely and its watermark backlog grew to 48,527 pending commits on
staging, leaving GET /api/accounts/{owner}/exists answering false for accounts
that exist on-chain.
Move both Sui pins to testnet-v1.80.0 (ceaaff1c), the current testnet release.
The remote store layout is unchanged between the two revisions (same
`{checkpoint}.binpb.zst` objects, same zstd + protobuf decode), so this needs no
ingestion or deployment configuration change and the saved watermark stays
valid.
Cover it two ways: `tests/affected_checkpoint.rs` decodes testnet checkpoint
384534623 — the one named in the incident, committed as a fixture in the exact
form the remote store serves — through the real `StoreIngestionClient`, and
`tests/checkpoint_decoding.rs` pins `Validity` to variant index 3 while holding
the earlier variants at their own indices, which already-indexed history
depends on.
(cherry picked from commit ca3f72d5e96e001c7cda67e43df321739b995696)
(cherry picked from commit 69908acc81fce294309d3c85b8ce9db5c87f404f)
Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
* fix(oauth): don't burn the session when Sui is unavailable (WALM-605) (#879)
* fix(oauth): don't burn the session when Sui is unavailable (WALM-605)
`session_complete` consumed the one-time authorization session and only
then verified the delegate key on-chain, mapping every
`verify_delegate_key_onchain` failure — a Sui `GetObject` 429 included —
to `invalid_request("delegate key is not registered on-chain")`. A
throttled fullnode therefore looked identical to a genuinely unregistered
key, and because the session was already spent the user could not retry:
they had to restart `/oauth/authorize` from scratch.
Follow-up to WALM-429 (#811), which taught signed HTTP auth the same
distinction but left this callback behind.
- Classify verify failures with `OnchainVerifyError::is_unavailable()`,
the same primitive `auth::cache_reverify_action` uses, so the OAuth
callback and signed recall cannot drift apart on what a 429 means.
- Move the atomic claim below the on-chain read: only a definitive answer
(registered or not registered) spends the session; an unreachable chain
leaves the row `pending` so the same request can be re-submitted.
Single-use is unchanged — no code is minted until the
`WHERE status = 'pending'` update wins, and the session-bound wallet
proof still gates everything above it.
- Return `temporarily_unavailable` 503 with `Retry-After: 5` instead of a
dead-end 400, matching signed auth's `AUTH_UPSTREAM_RETRY_AFTER_SECS`.
* review: let the consent UI re-send /complete alone (WALM-605)
Sparing the session only helps if something re-POSTs /complete.
ConnectClaude did not: any /complete failure went to the terminal error step,
and a reload re-ran handleConnect including POST /account, so a first-time
connect that had already landed add_delegate_key re-submitted the same
pubkey, aborted EDelegateKeyAlreadyExists (code 0), and the SPA reported
"this wallet is not the owner". The WALM-605 journey still dead-ended.
- fetchJson keeps the HTTP status and the OAuth `error` code.
- The /complete request moves behind completeSession(), which holds its
payload; on temporarily_unavailable / 503 the page shows a retry step that
re-sends that request and nothing else. Definitive rejections still land in
the terminal error state.
- Tests cover both: the retry path re-POSTs /complete once, issues no second
preflight and no second transaction; a 400 stays terminal.
- session_complete claims inside its match arms, so the Verified path no
longer carries an Option + expect() panic on a request path.
- Trim the before/after WALM-605 narration and the test-module essay.
* review: survive a reload during OAuth recovery (WALM-605)
The retry button worked, but it hung off a useRef. A reload dropped it, sent
the user back to consent, and the first-time path then re-ran handleConnect,
re-submitted add_delegate_key for a key already on chain, and aborted with
code 0 — which the SPA still reports as "this wallet is not the owner".
Reloading is the obvious thing to try when a page says "retry", so that was
still the reported WALM-605 dead end.
- The /complete payload is mirrored into sessionStorage under its session id,
so a remount resumes at the retry step and re-sends only /complete. The
entry is scoped to the session so one authorization's wallet signature can
never be replayed against another, and it is cleared on success.
- Auto-connect is suppressed while a recovery payload exists, so the resume
cannot fall through into a second /account.
- Retry copy no longer claims the key is registered. A 503 means Sui could
not be consulted, so it says the authorization is still pending instead.
- Tests: a reload resumes and completes with no second /account and no second
transaction; a payload from another session is ignored.
* review: do not leave a resumable crumb after a spent session (WALM-605)
The payload was written to sessionStorage at the start of every `/complete`
and cleared only on 200. A definitive 400 (Unregistered has already claimed
the row) therefore left it behind, and since `session_view` does not filter on
`status = 'pending'`, a reload showed the Sui-busy retry step for a session
that was already spent, looping there because nothing ever cleared the key.
- The ref is still set up front, so a hung in-flight `/complete` can resume.
sessionStorage is written only on the retryable branch, and the terminal
branch clears it.
- Test: a 400 followed by a remount stays on the error step.
- Corrected the comment that claimed the payload joins the existing
breadcrumb. It gets its own key precisely because the mount effect rewrites
`memwal_claude_connect` to `{ session }` and would drop it.
* fix(app): stop mapping add_delegate_key abort 0 to an owner mismatch (WALM-605)
`account.move` defines EDelegateKeyAlreadyExists = 0 and ENotOwner = 4, so
abort 0 means the key is already registered — not that the wallet is wrong.
That is the state a WALM-605 connect lands in: the tx went through and the
verify behind it hit a Sui 429, so re-running the flow re-submitted the same
pubkey and told the user to switch wallets.
Treat abort 0 as already-on-chain and go straight to /complete; report the
owner mismatch on abort 4, where it belongs.
* review: decide the duplicate-key skip on chain, not from the tx error (WALM-605)`alreadyOnChain` could never fire. ConnectClaude signs through
`useSponsoredTransaction`, which throws `new Error(sponsorFailureMessage(...))`,
and `sponsor::mask_upstream` collapses every upstream 5xx into a bare 502
`sponsor_upstream_error` that never echoes the upstream body. An Enoki dry-run
of a duplicate `add_delegate_key` therefore reaches the SPA as "The transaction
was rejected by the sponsor" — no abort code, no function name. The abort 4 and
abort 2 matches were dead for the same reason. The test that "covered" this
mocked a raw `MoveAbort ... abort code: 0` string, which is not what production
throws, so it passed while the user dead-ended.Move the decision to where the answer exists:- `/account` verifies this session's own pending key against the chain and
returns `resume_pending_registration`. Verified means an earlier attempt
landed the tx, so the UI skips straight to `/complete`.
- A Sui outage during that check returns a retryable 503 rather than guessing.
Registering walks into the masked 502; skipping sends `/complete` at a key
that is not there, which is definitive Unregistered and burns the session.
The preflight claims nothing, so a retry costs only that request.
- The classification goes through `classify_delegate_verify`, so the preflight
and `session_complete` cannot drift on what a Sui 429 means (WALM-429).
- `session_account` now sanitises `account_id` before the DB lookup and the
on-chain read, matching `session_complete`.Client:- Drop all three abort-code branches. The owner-mismatch guidance they carried
was unreachable; `session_complete` still reports a real mismatch as a 400.
- Write `PENDING_COMPLETE_KEY` before the request instead of only on a
retryable failure. A reload while `/complete` is in flight has no 503 to
react to, and that is the one gap the ref cannot cover. Terminal exits still
clear it, so a spent session cannot loop on the retry screen.
- `tx_digest` is `already-on-chain` when resuming, not `reused-delegate`.Tests:- The sponsor 502 is built from the real `SponsorHttpError` /
`sponsorFailureMessage` exports, so the fixture cannot drift from the
wrapper, and asserts `/complete` is NOT sent — folding every 502 into
"already registered" would push `ENotOwner` / `ETooManyDelegateKeys` into
`/complete` and burn the session.
- `/account` 503 submits no transaction.
- A reload with `/complete` still in flight resumes at the retry step with no
second signature and no second preflight.
- Four Rust tests pin the preflight decision, including that it and
`session_complete` agree on which errors are definitive.
* review: return a retryable /account 503 to consent (WALM-605)
A preflight POST /account 503 leaves the authorization session pending, but the catch sent it to the terminal error, which has no way back. Return to consent and show the reason so the user can start the request again.
* review: use the restyled error class on the retry screens (WALM-605)
The rebase onto dev dropped errorTextStyle. Both retry messages now use mcp-error, and the finish button uses mcp-btn.
---------
Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
* docs: list requestTimeoutMs and the accepted key formats (#1231)
The configuration reference and API reference described key as hex-only and omitted the request deadline.
* fix(sdk): reject an invalid analyze date in the mock (#1229)
MemWalMock.analyze accepted an Invalid Date that MemWal throws on. A valid anchor is still not rewritten; the mock has no extractor.
* fix(sdk): reject an invalid recall query, limit, or weight (#1228)
* fix(sdk): reject an invalid recall query, limit, or weight
The real client signed a blank query, a NaN limit, and a NaN scoring weight. Those now fail before the request.
* test(sdk): expect the positive integer recall limit error
dev already rejects limit 0 and below with TypeError. This branch keeps that check.
* fix(sdk): reject a non-array bulk status id list (#1226)
A string or null jobIds list threw TypeError in the mock. Both clients now throw the same validation error before any request.
* fix(sdk): reject a non-finite recall maxDistance (#1227)
NaN and a negative cutoff used to drop every hit and look like an empty namespace.
* fix(sdk): reject an object passed as remember text (#1230)
* fix(sdk): reject an object passed as remember text
remember, rememberAsync, and rememberAndWait throw TypeError before signing when text is not a string.
* review: reject an object remember text in the mock
MemWal throws TypeError. The mock used to fall through to "text cannot be empty".
* fix(sdk): turn an empty or non-JSON 2xx body into an SDK error (#1207)
* fix(sdk): turn an empty or non-JSON 2xx body into an SDK error
health() and signed requests called res.json() on success and leaked
a raw SyntaxError. Callers now get an error that names the HTTP status.
Fixes #1201
* fix(sdk): name empty JSON bodies in MemWalManual and MCP
The same 2xx parse guard from MemWal now covers manual restore, compatibility, embed, Walrus upload, and the MCP relayer check.
* fix(sdk): give an unreadable 2xx body its HTTP status
A remember wait treated a status-less parse error as HTTP 0 and polled until timeout.
* review: await the restore body and test the empty JSON path
Without await, the deadline disposed before text() finished, so a stalled body became a TypeError. The empty-202 tests now stub the SEAL session and hit /api/remember.
---------
Co-authored-by: HoangDucBach <hoangbach0985@gmail.com>
Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com>
Co-authored-by: ducnmm <165614309+ducnmm@users.noreply.github.com>
Co-authored-by: Hoang Duc Bach <124645604+HoangDucBach@users.noreply.github.com>
Co-authored-by: harrymove-ctrl <harry.phan@commandoss.com>
Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
Co-authored-by: harrymove-ctrl <267878750+harrymove-ctrl@users.noreply.github.com>
* Promote dev → staging (#1239)
* docs: show recall() scoringWeights on SKILL.md (WALM-616)
The TypeScript gap already shipped in 0.1.6. Agent docs still listed the old RecallOptions, which is how GH #708 concluded recency ranking was unreachable.
* docs: define ScoringWeights on SKILL.md (WALM-616)
Agents reading only this file still could not see the weight fields.
* fix(relayer): restart a failed same-key job on the insert-race path too (WALM-670)
* fix(sdk): say what the relayer last reported when a remember wait times out (WALM-670)
* fix(python-sdk): say what the relayer last reported when a remember wait times out (WALM-670)
* fix(python-sdk): say the retry reuses the job only on the same client (WALM-670)
* feat(sdk): listMemories() pages every memory without a query (WALM-388)
Recall is similarity-ranked and limit-bounded, so a write can't be
confirmed and a namespace can't be counted. listMemories() wraps the
existing GET /v1/owners/{owner}/memories and reuses resolveOwner().
Metadata only: no blob fetch, no decrypt.
The relayer has no namespace filter, so the optional namespace is
applied client-side; a filtered page can be empty while has_more is
true. Tombstone fields default for relayers older than WALM-363.
* fix(relayer): report the paid-recovery state a same-key retry actually reached (WALM-670)
A same-key POST on a paid job always answered 202 `uploaded`, even when it
lost the recovery claim. And the claim fenced `failed` rows for 60s, so a
recovery that had just failed (the wallet job, or an enqueue error that
returned before the publish) left a lease behind. The next retry enqueued
nothing, still said `uploaded`, and GET kept saying `failed`: the
poisoned-key shape for a blob that already exists.
The claim now publishes `uploaded` in the same statement. A `failed` row is
claimable at once, and only an `uploaded` row keeps the 60s fence. Two
racing retries therefore enqueue one recovery, and the loser reports the
row's real status. If enqueue fails, the claim is released back to
`failed` with the lease cleared, so the next retry takes it immediately.
* fix(sdk): tell a timed-out remember wait to keep waiting on the job id (WALM-670)
The timeout message said "retry with the same idempotency key" and that
remember_and_wait / rememberAndWait does this for you. Neither is reliably
true. `remember` drops its key as soon as the POST is accepted, and an
explicit idempotencyKey is never stored, so following the advice minted a
new key and a second paid write while the first job was still running.
A wait only times out on a job that is still pending, running or uploaded
(a failed job raises), so the advice is now to call waitForRememberJob /
wait_for_remember_job on the job id again, and to pass an explicit key
again on any retry. Python also stores the redacted server_error on the
exception attribute, not just in the message.
* fix(sdk): reject bad mock page sizes and honor recent recall (#1167)
* fix(sdk): reject bad mock page sizes and honor recent recall
MemWalMock.listNamespaces treated limit 0 as an endless page and a negative
limit as slice's drop-the-tail. Bulk writes skipped the client's empty-batch
check, idempotency keys were ignored, and sort recent stayed oldest-first.
Positional recall(query, options, namespace) also dropped the namespace on
both the mock and MemWal.
Fixes #1144, #1145, #1146, #1147, #1124, #1038
* test(sdk): cover the mock limit, idempotency, and recent-recall edges
The first tests matched the issue repros. These also lock the branches a
tie-break-only sort, a 50-row window cap, a byte-length key, and a mutated
options object would have left green.
* fix(sdk): treat a remember options object as options and fix job status polls (#1168)
* fix(sdk): treat a remember options object as options and fix job status polls
remember(text, { idempotencyKey }) and rememberAndWait used to send that object as the namespace. Empty job ids no longer hit GET /api/remember/. Duplicate ids in waitForRememberJobs settle in every slot. Bulk status returns namespace, status polls skip the SEAL session, batches over 20 are rejected before signing, and a destroyed client throws instead of reading a zeroed key or probing /version.
#1142 #1087 #1121 #1084 #1104 #1135 #1136 #1090
* fix(sdk): reject a destroyed key still in flight and merge remember options
An in-flight getPublicKeyHex resolved to the zeroed cache, and a key
derived after destroy was the well-known key of the wiped seed. A third
options argument was also dropped when the namespace slot was an object.
* fix: reject blank text, treat [::1] as local, and pin weekday dates (#1134)
* fix: reject blank text, treat [::1] as local, and pin weekday dates
Whitespace-only remember, analyze, and embed text is rejected before a job is created (#1129). The TypeScript client treats bracketed ::1 as loopback (#1128). Fact extraction copies a server-built weekday calendar (#1131). AUTH_REJECTED names a pasted wallet address without changing the 401 (#1132).
* refactor: one weekday table, inline the loopback host check
The English weekday names lived in a match and again in the calendar loop. One Sunday-first array covers both, indexed by num_days_from_sunday. The IPv6 bracket strip stays inside normalizeServerUrl.
* fix(server): return each blob once in recall (WALM-594)
…
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Promotes
devintostaging, frozen at256ac7f0. The merge result is that tree.#1182 was squash-merged, so this PR's commit list does not share those commits and GitHub shows 126 files. The staging tree already contains that promotion. Against the current staging tree, only #1235 is new:
services/server/scripts/sidecar/enoki.tsservices/server/scripts/sidecar/routes/walrus-upload.tsservices/server/scripts/__tests__/walrus-wal-destroy-zero.test.tsChanges
reserve_spaceaborts inbalance::split. The sidecar drops the shared Walrus client, same as the price-dropdestroy_zeropath, so the next attempt re-reads the live price.Not in this promotion
devdev(fix(sdk): reject zero-width space and format characters in assertNonBlankText (Fixes #1156) #1177, fix(python-sdk): default server_url to the hosted relayer #1232, fix(python-sdk): forward recall scoring weights and sort #1233, fix(python-sdk): wrap POST /api/forget #1234, fix(chatbot): fail loudly on a weak or missing AUTH_SECRET #644, ci(openclaw): run the plugin unit suite per PR and the E2E suite on staging/main #683, fix(mcp): deduplicate bulk writes on bridge reconnect replay #1046, fix(ci): retry a prerelease publish that hits a staged version #1237)Test plan
256ac7f0balance::splitretries against a refreshed Walrus client