Skip to content

Promote dev → staging - #1182

Merged
harrymove-ctrl merged 91 commits into
stagingfrom
dev
Oct 16, 2026
Merged

harrymove-ctrl merged 91 commits into
stagingfrom
dev

Conversation

@nikola0x0

@nikola0x0 nikola0x0 commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Promotes dev into staging: 49 PRs since #1122, frozen at 34e520fd. Carries the indexer Validity fix (#1224), per-namespace idempotency keys in safety mode (#1176, #1225), listMemories() (#1020), and the SDK and Python fixes merged since the last promotion.

Safety mode: this release adds the new idempotency index but keeps the old one, so the relayer can still be rolled back. Per-namespace keys go live next release, when the old index is dropped. Until then, reusing a key in a second namespace returns a 500 instead of a 409.

Changes

Relayer / indexer

Security

SDK

Python SDK

MCP / app / docs

Not in this promotion

Test plan

  • fix(indexer): decode checkpoints carrying the Validity expiration #1224 and fix(server): hold back migration 025 until the next release (WALM-856) #1225 merged into dev first
  • CI green on this PR at 34e520fd. The Release SDK publish job first lost an npm version race when five PRs merged within seconds (409 on 0.1.8-dev.23); the re-run published 0.1.8-dev.24.
  • Staging relayer boots; remember_jobs has both idempotency indexes, both valid
  • Staging relayer: same-key retry in one namespace returns the original job; recall on a fresh namespace returns each blob once
  • Staging indexer advances past a Validity checkpoint; /api/accounts/{owner}/exists is true for a new account
  • Staging app: /connect/mcp from a real MCP login completes, a hand-built link is refused, the dashboard loads
  • Open staging → main once the above pass

HoangDucBach and others added 24 commits September 11, 2026 11:17
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.
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
@ducnmm
ducnmm deployed to benchmark-dev October 12, 2026 09:31 — with GitHub Actions Active
@HoangDucBach
HoangDucBach deployed to benchmark-dev October 12, 2026 09:49 — with GitHub Actions Active
@ducnmm
ducnmm deployed to benchmark-dev October 12, 2026 10:03 — with GitHub Actions Active
…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
@railway-app
railway-app Bot temporarily deployed to Walrus Memory / dev October 14, 2026 13:56 Inactive
…#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.
@railway-app
railway-app Bot temporarily deployed to Walrus Memory / dev October 14, 2026 13:57 Inactive
ducnmm and others added 4 commits October 15, 2026 07:59
* 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>
ducnmm and others added 9 commits October 15, 2026 11:28
…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.
@harrymove-ctrl harrymove-ctrl mentioned this pull request Oct 16, 2026
6 of 7 tasks
@ducnmm ducnmm mentioned this pull request Oct 16, 2026
2 tasks
@ducnmm ducnmm mentioned this pull request Oct 21, 2026
10 of 15 tasks

This branch was successfully deployed

1 active and 1 inactive deployments
benchmark-dev — c59e679f Deployed Oct 16, 2026 by harrymove-ctrl via SDK / E2E (live relayer) #97
Walrus Memory / dev — c59e679f Deployed Oct 16, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants