Promote staging → main - #988
Merged
Merged
Conversation
A remember is a background job (pending -> running -> uploaded -> done) and the tool waited for `done` with a 90s deadline. Measured against production that costs 30-75s for a single short fact, while the job is durably accepted about a second in. Phase timing on one job: accepted 1.2s, pending->running 2.1s, running->uploaded 29.6s (84% of the call), uploaded->done 2.1s. Payload size does not drive it — a 1.2 KB fact finished faster than a 97-character one — so the agent spends half a minute waiting on a queue it cannot affect. memwal_remember now waits MEMWAL_MCP_REMEMBER_WAIT_MS (default 10s) for the write to land, so the fast path still returns a blob_id, and otherwise hands back the job_id and says plainly that the fact is not saved yet. Set 0 to always return at accept, or 90000 to restore the previous behaviour. Returning early cannot mean losing writes: a job can still fail after it is accepted — an outage on the SEAL encrypt sidecar lands it in `failed` with "Memory encryption backend is unavailable" long after the tool returned. So memwal_remember_status resolves an in-flight job by id, reporting a failed or missing job as an error envelope rather than a status line, and a genuinely failed job still reaches the agent as an error from memwal_remember itself. Only our own wait expiring is treated as a non-failure.
The fast-return path keyed "job is still running" off a `MemWalRememberJobTimeout` constructor name. No shipped SDK throws that: both the pinned 0.0.x and the current 0.1.x line reject with a plain Error carrying `status` — 504 when the wait ran out with the job still going, 500 when the job itself failed. `wrapTool`'s mapping for those names is dead code for the same reason. So every slow write came back as `isError: true` with "Tool error: remember job timed out after 30000ms", telling the agent the save had broken when it was simply still running — the exact failure mode the fast return exists to avoid. Caught by driving the tools against the production relayer; the unit tests missed it because the stubs threw a conveniently-named class instead of the shape the SDK actually produces. Match on `status` instead, and reproduce the real error shape in the stubs. memwal_remember_status now also names a failed job (500) plainly rather than letting it fall through to the generic "Tool error" prefix.
The 10s wait this branch shipped was the worst of both options. Against the measured 30-75s completion spread it lands in the pending branch on nearly every call, so the agent pays the full 10s and still gets no guarantee. Wait long enough to mean it (MEMWAL_MCP_REMEMBER_WAIT_MS=90000) or do not wait — so the default is now 0 and the tool returns once the relayer has durably accepted the job, ~1.1s. Returning at accept is safe from a disconnect: the job is a row in remember_jobs driven by the relayer, not work held in this process. It is not safe from a job that fails after acceptance, which is why the result never reads as saved and memwal_remember_status exists to settle it. Split the shared budget parsing and job-error mapping into remember-wait.ts rather than duplicating the 504-vs-500 discrimination in both tools. memwal_remember_status gains waitMs=0 for an immediate read. waitForRememberJob cannot express that — it sleeps before its first poll, so a 0ms deadline answers "still running" without ever asking the relayer. That needs getRememberStatus, added to the SDK in 0.0.4, hence the dependency bump. The wider 0.1.x upgrade stays with the sidecar SDK work. Also adds the Streamable HTTP transport behind MEMWAL_MCP_TRANSPORT (sse by default). It answers a call on the same request, so there is no idle watchdog and no replay-on-reconnect. Tests: 269 run, 236 pass, 33 fail — the same 33 sandbox port-binding failures present on dev. tsc --noEmit clean.
Round-robin `next_index()` hands out the next wallet in sequence whether or
not it is mid-upload. The sidecar allows one upload per wallet
(WALRUS_UPLOAD_PER_WALLET_CONCURRENCY defaults to 1), so landing on a busy
wallet costs the full upload ahead of it while other wallets sit idle.
Observed in production as a job waiting 6.2s for its assigned wallet with the
global semaphore reporting available:2, queued:0 — the contention was never
global, only per-wallet:
[walrus/upload] limiter_acquired {"keyIndex":6,"waitMs":6221,
"limits":{"global":{"capacity":8,"available":2,"queued":0},
"perWalletCapacity":1,"wallet":{"capacity":1,"available":0,"queued":1}}}
`least_loaded_index()` picks the wallet with the fewest in-flight attempts and
falls back to round-robin ordering among equals, so an idle pool still spreads
evenly and a fully busy one does not pile onto one signer.
Marking a wallet busy is a `WalletAttemptGuard` rather than paired
increment/decrement calls: the upload path has many early returns, and every
one of them has to release the slot. A guard cannot forget.
Tests: 11 new unit tests covering selection, guard release on early return,
nested attempts, saturating release, and an out-of-range index. Lib suite goes
466 → 477 passing with the same 21 pre-existing failures (no database in the
sandbox). cargo check clean, rustfmt clean on the touched files.
The sidecar installs @mysten-incubation/memwal from npm (Dockerfile runs
`npm ci` in scripts/), so packages/sdk in this repo is not what production
runs — and the pin had drifted far behind it. 0.0.4 predates every release
that matters here; most importantly it has no idempotency support at all,
so `rememberAsync` cannot send an idempotency_key and the relayer treats
every retry of a write as a brand-new one. A replayed remember therefore
mints a SECOND paid Walrus blob for a write already in flight. The SDK
grew keys in 0.1.2 ("collapse retries onto one paid remember job") and
production never received it.
0.1.7 is the current published version. The MCP layer only calls
analyzeAndWait / health / recall / rememberAndWait / rememberBulkAndWait /
restore, and deliberately excludes the manual-mode methods that carry the
one breaking change in the range (0.1.5 reshaped rememberManual), so the
used surface is unchanged. Verified by `npm ci` against the regenerated
lockfile followed by a clean `tsc --noEmit` over scripts/.
Also picks up the widened zod peer (^3.23.0 || ^4.0.0), which the
sidecar's own zod ^3.25.0 already satisfied.
memwal_remember no longer blocks to terminal, but memwal_remember_bulk and memwal_analyze still do, and both were left on the SDK's default cadence. That default backs off as min(10s, 1500ms * 1.5^attempt), so status checks land roughly 1.5, 3.75, 7.1, 12.2, 19.8 and 29.8s apart. Writes finish in the 15-35s band, which is exactly where those gaps are widest: a batch that truly completed at 20.5s is not reported until 29.8s. None of that is work, it is waiting to be told the work finished. Reuses REMEMBER_POLL_INTERVAL_MS rather than picking a second number — the rate-limit reasoning behind 400ms is the same one, and a bulk poll covers every pending job in one request (/api/remember/bulk/status takes all the ids), so the request budget matches a single remember's.
The sweeper applied one callTimeoutMs to every tracked request. That number is DEFAULT_CALL_TIMEOUT_MS, sized for memwal_analyze — the slowest tool there is — so a memwal_remember whose reply was lost (the relayer answered, the stream dropped before it arrived) kept the agent blocked for 240s even though that tool cannot still be working: it gives up on its own job long before. Users read a four-minute silence as a hang and reload the client, which is the reload-for-minutes symptom. Each entry now carries the deadline its own tool enforces plus 30s of transport headroom, fixed when the request is first tracked so a reconnect replay keeps the original budget. memwal_remember lands at 120s. The stalled-handshake shortcut still wins when it is tighter but can no longer extend a call past its tool's ceiling. Deliberately generous headroom: the sidecar answers at its deadline with a result or an error envelope rather than going quiet, so a reply is one hop behind it. Cutting a merely-late reply off early is the expensive mistake, because the agent then retries a write that actually landed. Behaviour is unchanged wherever MEMWAL_MCP_CALL_TIMEOUT_MS is set, which is every existing expiry test — resolveDeadlineMs returns the override as-is, and in the stalled path min(stalledHandshakeMs, callTimeoutMs) is already stalledHandshakeMs. Also drops the duplicate local toolNameOf in favour of the module-scope one this needs.
Three changes to what a caller waits for, none to what a write means. The backoff ceiling drops from 10s to 2s and the base default from 1500ms to 600ms. The ceiling is pure observation cost — a job that finished is not reported until the next poll lands — and at 10s the checks fell at ~1.5, 3.75, 7.1, 12.2, 19.8 and 29.8s, straddling the 15-35s band where writes actually complete. Polling is one indexed row read on remember_jobs, so the extra checks are cheap; callers on a long budget still pass a larger base because the relayer's per-delegate-key rate limit, not cost, is the real constraint. Both wait loops slept BEFORE their first check, so an idempotent replay of a write the relayer had already finished paid a full interval for a result that was ready on arrival. They now check first and sleep second. Generated idempotency keys are derived from the content over a 30-minute bucket instead of crypto.randomUUID(). pendingRememberKeys only dedupes retries that reuse one client instance, and the MCP sidecar builds a fresh MemWal per transport session — so a reconnect replay found an empty map and a random key read as a brand-new write, minting a second paid Walrus blob for one already in flight. The bucket bounds the collapse: remember_jobs rows are never pruned, so an unbucketed key would dedupe against a job from any point in history and re-saving a since-deleted fact would hand back the old blob id instead of storing it again. Callers passing an explicit idempotencyKey are unaffected, and distinct text or namespaces still derive distinct keys.
memwal_remember stopped blocking on the whole Walrus write; bulk did not, and bulk is the path an agent actually takes. The server instructions send it here whenever it learned more than one thing, so leaving this tool on a fixed 120s block meant the common multi-fact turn still stalled — the fast return only covered the case that mattered less. Blocking is also worse per fact here, not better. A batch is N separate Walrus writes contending for the same upload slots (WALRUS_UPLOAD_PER_WALLET_CONCURRENCY defaults to 1), so they land one after another. Against the measured 30-75s single-write spread a five-fact batch could exhaust the whole budget and come back as nothing but timeouts, having held the agent for two minutes first. Same shape as memwal_remember: rememberBulkAsync to accept, then hand back the job_ids. Safe on the same grounds — remember_bulk commits every remember_jobs row before it spawns preparation or answers (services/server/src/routes/remember.rs), so acceptance survives the client going away. Each job_id is returned paired with its fact, because with a batch "one of these failed" is only actionable if you can tell which. A non-zero MEMWAL_MCP_REMEMBER_WAIT_MS still waits, and that path now reports a mixed batch honestly: waitForRememberJobs does not throw on expiry, it marks stragglers `timeout`, so those are shown as still uploading with their job_ids rather than as failures. memwal_remember_status takes job_ids to settle a whole batch in one call. It deliberately does not throw on a failed job the way the single-id path does: a batch comes back mixed, and throwing on the first failure would discard the blob_ids of the writes that did land.
memwal_remember was observed still running past 120s by an MCP client, on a tool documented as capping at 90s. The cap is not a bound. The SDK's signedRequest aborts a request only when the caller hands it a signal, and of the memory methods only recall() does (15s). rememberAsync, rememberBulkAsync and every job-status read call it with no signal, so the underlying fetch has no deadline. waitForRememberJob then tests `Date.now() < deadline` at the TOP of its poll loop, which bounds when the next poll starts, not how long one takes — so a single stalled socket runs as long as it stays open and sails straight past timeoutMs. With nothing else in the way the stdio bridge's orphan sweeper is the first thing to fire, minutes later, which is what a user sees as the client hanging. Returning at accept does not fix this on its own: the accept POST is one of the unbounded calls, so memwal_remember could hang indefinitely even at MEMWAL_MCP_REMEMBER_WAIT_MS=0. Every entry point now runs under a deadline — accepts at 15s (the only deadline the SDK sets for itself; a healthy accept is ~1.1s), waits at their own budget plus grace, since the budget cannot bound its own last poll. The request is NOT cancelled: the SDK exposes no way to pass a signal, so fetch keeps running until it settles. What this bounds is how long the agent waits, which is the part a user experiences as a hang. An orphaned request costs one socket and resolves into a promise nobody reads. The timeout error is named MemWalRelayerUnresponsive rather than reusing a job-failure name, because "we do not know whether it was queued" is not "it failed" — and it says a retry is safe, since the SDK holds the same idempotency key until an accept succeeds, so a retry attaches to the existing job instead of queueing a second paid copy. Also documents MEMWAL_MCP_REMEMBER_WAIT_MS, which this branch introduced without an entry.
`fetch` has no timeout of its own, and this SDK passed an abort signal on exactly one method — `recall`, at 15s. The accept POST, every job-status read, and the `/version` and `/config` handshake calls that run before any of them could stay pending for as long as the socket stayed open. That is not merely untidy. A poll loop tests its budget at the TOP of each iteration, so it bounds when the next request starts, not how long one takes: a single stalled read ran straight past `timeoutMs`. A `memwal_remember` documented as capping at 90s was seen by an MCP client still running after 120s, with the stdio bridge's orphan sweeper the first thing to fire, minutes later. The MCP layer now wraps its own calls, but that only covers one consumer — the hole is here. Default 30s, matching the relayer's own outbound HTTP client: any call that needs the relayer to reach the sidecar, Walrus or OpenAI has already failed upstream by the time it fires. Settable via `requestTimeoutMs`; a non-positive or non-finite value falls back to the default rather than disabling the bound, since "no deadline" is the bug being fixed. Two endpoints legitimately outrun it and say so: `restore` (60s — the route bounds itself at 55s server-side and answers with an error rather than going quiet, so a tighter client deadline would abandon a reply already on its way) and `analyze` (60s — it runs the extractor LLM inline before it accepts). `recall` keeps 15s, now a named constant instead of a hand-rolled AbortController. Each poll inside a wait loop is bounded by the client deadline clamped to the remaining budget. Both directions carry weight: the remaining budget stops a poll outliving the wait it belongs to, and the client deadline stops ONE stalled poll swallowing the whole budget, which would leave the loop no room to retry. Expiry raises `MemWalRequestTimeout` with `status: 504` — already transient to `isTransientPollingStatus` — so a stalled poll is retried against what is left instead of failing the wait. An abort the caller asked for, and any other transport error, propagates untouched: an operator debugging DNS or TLS needs the original error, not "timed out".
`remember` commits the job row, then spawns preparation — summarize, embed, SEAL encrypt, enqueue — in a `tokio::spawn` inside the relayer process. If the process stops in that window the row is left at `pending` with nothing to resume it, and the sweeper never looked at `pending` at all. The state machine in migration 005 does not even name a `pending → failed` edge: the state had no exit. Nothing surfaced it either. `memwal_remember_status` read the row and reported "still uploading" indefinitely, so a write the user was told was on its way was simply gone. Returning at accept made that worse rather than causing it: the blocking call at least ended in a timeout the caller saw, where now nobody is waiting to notice. `pending` cannot be swept wholesale — a job that IS prepared waits at `pending` until a wallet worker takes it, and with WALRUS_UPLOAD_PER_WALLET_CONCURRENCY defaulting to 1 that queue is legitimately minutes deep, so failing those would abandon paid work about to run. `preparation_encrypted_b64 IS NULL` separates them: it is written by the statement immediately before `enqueue_wallet_job`, so its absence means the job never reached the queue. Failing is the only available outcome, not a preference. The row holds the SEAL ciphertext and never the plaintext, so a job that died before encrypting has nothing to retry from — the error says the fact was never stored and must be sent again, rather than implying a job merely died. Clearing `prepare_claim_token` is what makes this safe against a preparation that was slow rather than dead: that task's own UPDATE is fenced on the token, so it now matches zero rows, logs the lost claim and returns before `enqueue_wallet_job`. It cannot mint a paid blob for a job just declared dead. Quota needs nothing extra — `main` already runs `release_reservations_for_terminal_jobs` immediately after this on the same tick, ordered that way so rows this pass just failed are reconciled without waiting another minute. Composes with the existing idempotency recovery: a failed row with no blob_id is exactly what the remember route resets and re-prepares, so a client that does retry the same key gets a real second attempt instead of collapsing onto a corpse. Migration 005's comment still lists the old transitions. It is deliberately left alone — sqlx checksums migration files, so editing a shipped one breaks `migrate` on every deployed database; the transition is documented on the sweeper instead.
…M-470) (#911) * fix(server): stop scoring_weights reordering an explicit recall sort (WALM-470) A recall request carrying both sort and scoring_weights returned neither order: select_hits_for_sort ordered and truncated the hits, then the ranker reordered the survivors by composite score, so sort=recent stopped meaning newest-first. Per the decision on WALM-460, an explicit sort is the order and weights apply only when sort is omitted. RecallRequest.sort becomes Option<RecallSort> so omitted and "relevance" differ, and resolve_scoring_weights validates the weights, then suppresses them when sort is set. The SDK docs and both API references state the rule. * fix(server): narrow the embedding size check to the provider path (WALM-470) The WALM-423 check ran before the embedder looked for an API key, so deployments without one (local dev, CI, self-hosted) rejected remember and recall text over 16 KiB, though the key-less fallback hashes locally and has no context window. It now runs only when a provider key is set. The same change mapped every provider 400 to "input exceeds the model context limit". A 400 now becomes BadRequest only when its body names a context-length problem; anything else, such as an unknown model id, stays Internal so it is not blamed on the caller and still alerts. The embed call moves into embed_text, which takes only the key, base URL and text, so the tests can drive it against a local stand-in provider. * docs(sdk,server): add the 0.1.7 changelog entry and trim WALM-470 comments Review follow-up: record the recall sort precedence change under the unreleased 0.1.7 in both SDK changelogs (no version bump), and drop ticket ids, dates and design history from the new code comments. --------- Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com>
…s (WALM-608) (#910) * fix(relayer): point the upload-queue saturation alert at real counters (WALM-608) The saturation monitor read queuedWalrusUploads, activeWalrusUploads and walrusUploadLimits.globalCapacity from the sidecar's /health. Since cef9728 (v1_new port, 31 Jul) /health is bare liveness and returns none of them, so every read fell through to unwrap_or(0) and the alert could never fire. /ready has the counters but waits on Sui, Walrus and an uncached archival GraphQL query, each with a 5s timeout, against the monitor's 2s budget. Backlogs arrive with Sui RPC pressure, so polling it would go blind exactly when the alert matters. - sidecar: add GET /metrics/uploads, serving the in-memory counters and limits with no auth, in both route modes - relayer: poll it; move parsing and the consecutive-check state into sidecar_saturation.rs; a missing or non-integer field, a non-JSON body or a non-2xx status logs an error instead of reading as an empty queue - docs: list the endpoint and the built-in alert in relayer observability * docs(relayer): trim WALM-608 comments to what the code guarantees Review follow-up: drop the incident history from the route, test and parser comments; the PR body keeps it. --------- Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com>
…pted [WALM-332] (#793) * feat(server): add GET /api/whoami so a delegate key can resolve its own account A client that holds a delegate key but lost the surrounding metadata has no way to rebuild credentials: `account_id` is required locally, and nothing exposed it. `find_account_by_delegate_key` is internal to the auth middleware, `account_exists` takes an owner address and returns only `{exists}`, and `StatsResponse` carries `owner` rather than the account id. The middleware already resolves exactly what is needed while authenticating, so this hands back what it computed instead of doing new work: `account_id` and `owner` from the registry scan, `package_id` from config. Returning `account_id` is safe here precisely because the route is authenticated — the caller proved possession of a key registered against that account, so it only ever learns about itself. The public existence-check route deliberately withholds it, and that reasoning is unchanged. The field mapping is factored into `whoami_response` so it can be tested without a live AppState (this codebase has no axum-handler harness). `account_id` and `owner` are both 0x-prefixed 32-byte hex, so transposing them would compile and silently hand back the wrong identity — pinned by a test. Motivated by WALM-332. * fix(mcp): stop losing the delegate key when a login is interrupted The browser registers our delegate public key on-chain — a paid, irreversible action — and only afterwards POSTs the callback that makes us save the private half. Until now that private half existed solely in memory (`login.ts` created it, `saveCreds` persisted it only on success), and the callback listener lived in the same process. So any death in that window destroyed the only copy of a key that had already been paid for and committed on-chain, leaving an orphaned registration nobody could use. Nothing reported it: the browser's POST hit a closed port, and the process was gone so it logged nothing. `handleLocalLogin` had already returned the URL and told the client the call succeeded. Reproduced against the real binary over stdio: preflight succeeds while alive (200), the process is killed, and the callback POST gets ECONNREFUSED. Ports are never reused across restarts, so the stale tab cannot reach a new listener either — it fails at /preflight, never reaching the state check. Note this is NOT the state-nonce mismatch WALM-332 originally described. That path is unreachable: the callback handler gates `preflightVerified` before it compares state, so a bad-state 403 requires the same live process to have accepted a preflight carrying its own nonce. The fix is write-ahead. Persist the keypair before anything can hand the public key to a browser, and clear it once the key is safely in credentials.json. On the next start a stranded record is reclaimed via the new authenticated `GET /api/whoami`, which supplies the account metadata the lost callback would have carried. Two deliberate constraints: - A rejected key is never deleted. A 401 means "not registered" on mainnet, but on testnet the registry scan is disabled outright and a genuinely registered key is rejected for want of an x-account-id hint. Deleting would destroy a paid key in exactly that environment, so the record waits out its 24h TTL instead. - Recovery never rolls back a newer sign-in. If the user gave up and signed in again, adopting the older stranded key would silently downgrade them. Also fixes the swallowed failure in `startOrReuseLoginFlow`: a login that fails while the process is still alive (timeout, listener error) was eaten into a `warn` and never reached the client. It now logs at error, writes to stderr, and emits an MCP notification so the agent stops waiting on a dead flow. The canonical signature string is duplicated across Rust and TypeScript because they cannot share code. A silent drift there would fail only in production, as an opaque 401, so the exact literal is pinned by a test on each side with a comment pointing at the other. WALM-332. * docs(relayer): drop em dashes from the whoami section The Sui docs style guide disallows em dashes in prose. Split the first aside into its own sentence and parenthesized the second; wording is otherwise unchanged. * fix(mcp): make the WALM-332 recovery path actually able to reclaim a key Review follow-ups on WALM-332. The write-ahead record was in place, but nothing downstream of it worked. - `whoami` signed `String(Date.now())`. The relayer freshness-checks `x-timestamp` against `Utc::now().timestamp()` — seconds — so a millisecond value was always outside the drift window and every recovery attempt 401'd with ERR_TIMESTAMP_OUT_OF_BOUNDS. Recovery could not have succeeded once. - Every non-200 was `rejected`, which tells the user to sign in again and revoke the key. That is the wrong action for a 503 `AUTH_UPSTREAM_UNAVAILABLE`, a 429, or a 404 from a relayer too old to serve the route — the key is fine, and re-registering costs gas for nothing. `rejected` is now 401/403 without the upstream-unavailable marker; everything else is `unavailable` and retried. - Every `loginFlow` minted a keypair and overwrote `login-pending.json`. Recovery only runs at process start and is skipped for `--login`, so a timed-out login followed by `memwal_login` in the same process replaced the only copy of a key the browser may already have paid to register. A still valid record for the same relayer is now reused, `createdAt` included so the TTL keeps measuring from the attempt that may have registered it. - Logout cleared `credentials.json` and left the pending record, so the next start recovered from it and signed the user back in. Both logout paths now clear it. Deliberately not folded into `clearCreds`, which also runs on 401 session teardown where a newer stranded key is what recovery still needs. - `savePendingLogin` swallowed write errors, restoring the original loss with no log line. It now logs and throws: the invariant is that the key is durable before its public half can reach a browser, and a directory that cannot take this file cannot take `credentials.json` either — that login was going to fail at the callback anyway, one paid `add_delegate_key` later. On the reviewer's suggestion to exempt `GET /api/whoami` from the testnet `x-account-id` requirement: that gate is not policy. The registry scan behind it runs over Sui JSON-RPC, which testnet no longer serves (auth.rs "Strategy 3"), so exempting the route would only route it to a retired endpoint. Documented as mainnet-only in the API reference instead. Also reattached the orphaned `whoami` JSDoc and skipped the pending-record mode assertion on Windows, where the bits are not enforced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsS2mBzMfpzy3QE8EMiKvS * docs(relayer): drop the em dash from the whoami network note Style-guide audit: no em dashes in prose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsS2mBzMfpzy3QE8EMiKvS * fix(mcp): stop pointing a failed sign-in at revoking the key it can reclaim Review follow-ups. The logout comment justified keeping `clearPendingLogin` out of `clearCreds` by saying `clearCreds` also runs on 401 session teardown. It does not: this tree deliberately refuses to wipe credentials on a relayer 401 (creds-wipe DoS, bridge.ts). The two logout paths are its only callers. Reworded to the reason that is actually true — discarding a reclaimable key is a decision only an explicit sign-out gets to make, and `clearCreds` is exported. Renamed the test that repeated the same false claim. The login-timeout message told the user to remove unused keys from the dashboard. Write-ahead exists precisely so that key survives, and revoking it destroys what the next start would reclaim. It now points at the two paths that work: run login again and the same key is reused, or restart and it is reclaimed. The `rejected` stranded notice had the same defect in the other order, telling the user to sign in again and then revoke. With keypair reuse that is self-defeating: the sign-in adopts that very key. Reworded. `superseded` keeps its revoke advice, which is correct there — that record is cleared, so the key really is dangling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsS2mBzMfpzy3QE8EMiKvS * docs(mcp): move the WALM-332 note into the unreleased 0.0.13 section Review asked for a 0.0.12 -> 0.0.13 bump across package.json, the verify script and the six plugin/marketplace manifests. origin/dev has since done exactly that itself, for WALM-480: every manifest, the verify script and both changelogs are already on 0.0.13, and npm still has `latest` at 0.0.12 with only a `0.0.13-dev.0` prerelease published. So 0.0.13 is open, not released, and this change belongs in it rather than in a further bump. Merged dev and moved the #793 note out of the shipped 0.0.12 section into 0.0.13. docs/mcp/changelog.mdx gets the same entry, plus the release summary and the `answer` frontmatter the reviewer flagged as missing. Also capitalizes Testnet in the whoami note, the one style-guide violation still outstanding from the docs audit. * docs(mcp): apply the style-guide wording, and fix three stale comments Review nits. The audit re-ran on 71981f4 and still flagged the 0.0.13 bullet: `on-chain` -> `onchain` (docs run 155 to 19 that way), and two passive constructions. Applied to `packages/mcp/CHANGELOG.md` and `docs/mcp/changelog.mdx` together so the two stay byte-identical, and to the `answer` frontmatter, which carried the same hyphenation the audit does not scan. Three comments still described advice ab1636b replaced: - `recovery.ts` — the denied/unavailable split justified `rejected` by advice ("sign in again, then revoke the key") that the branch no longer gives. The reason still holds under the new copy, so it now states that one. - `recovery.ts` — `formatStrandedLoginNotice`'s JSDoc called revocation *the* actionable step. It is now the abandon path only; naming the key serves both. - `bridge.ts` — the logout comment pointed at "the relayer-401 handling below". It is above: the module doc, and the SSE 401 path. `superseded` keeps its revoke advice, which is correct there. No user-facing string changed, so the tests pinning the unavailable copy are untouched. * fix(mcp): send an approved stranded key to a restart, not a revoke or a retry loginFailureNotice and the troubleshooting page still told the user to remove the key from the dashboard and sign in again. That revokes the only copy a restart would reclaim. "Sign in again, the same key is reused" is not a fix for an approved key either. ConnectMcp.tsx always sends add_delegate_key, and the contract aborts on a key that is already registered. So the notice, the timeout reason, the bridge warning, the rejected-recovery notice and the troubleshooting page now split on whether the wallet step was approved: approved means restart to reclaim, not approved means sign in again, and the dashboard is only for abandoning the key. On Testnet, where reclaim cannot confirm the key, remove it first and then sign in. The notice and the rejected-recovery notice are pinned by tests. * fix(mcp,app): stop the failure surfaces retrying a key only a restart reclaims Two surfaces still contradicted the notice they sit next to. auth-required appended the generic LOGIN_INSTRUCTION after a failed sign-in, so the same blob said "restart the MCP client and it is reclaimed" and "no terminal command, no client restart", and led with memwal_login for a key that cannot be registered twice. A failure now gets a retry instruction scoped to the case it fixes: the wallet step was never approved. The concatenated blob is pinned so it cannot say both. The dashboard's callback-failed card told the user to sign in again and remove the unused key, which throws away the registration a restart would reclaim. It now points at the restart, and at the dashboard only for abandoning the key, matching the troubleshooting page it links to. --------- Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The session instructions are what actually steer an agent, and they still described only memwal_remember as returning ACCEPTED-but-not-saved. Now that memwal_remember_bulk behaves the same way, an agent reading this would take a pending batch for a stored one and tell the user their facts were saved. Also names the two things that only apply to a batch: its writes are stored one at a time rather than together, so it takes longer than a single fact, and memwal_remember_status accepts job_ids to settle the whole batch in one call rather than one id at a time.
…econd Apalis attaches no retry/backoff layer, so a retriable upload error is re-polled almost immediately. Production shows what that costs: one job took attempts 2, 3, 4 and 5 against Walrus `503 Too Many Requests` inside a single second, rotated through four wallets, and died as "exhausted retries" about a second after its first failure. The upstream limit is time-based, so rotating wallets cannot help — only waiting can, and nothing was waiting. Reuse the existing `backoff_duration` schedule (2s, 4s, 8s, 16s) between upload attempts, the way the lock-defer path already does. The wallet slot is held across the sleep on purpose: a backing-off job is still that wallet's turn, and releasing it would invite another job onto a wallet about to retry. `upload_retry_backoff` returns None for an aborting error and for the final attempt, so nothing sleeps when no retry is coming. 19 of the 24 hours of upload failures sampled were this one rate-limit, and roughly 23% of jobs reached a second attempt.
`memwal_remember` now returns as soon as the relayer accepts the job, which leaves a window where the write still dies — a SEAL encrypt outage, an exhausted upload budget — with nobody listening. `memwal_remember_status` answers for one job, but nothing obliges an agent to ask, and storing a memory is typically the last thing it does in a turn. An unasked question is the same as a silent loss, and for a product whose promise is durable memory, silent loss is worse than slow. Recall is the call an agent always makes, so the bad news rides along there. `/api/recall` now carries `failed_writes` — this owner's writes that reached `failed` in the last 24h, capped at 5 — and the MCP tool renders them as a warning naming the job, the namespace and the relayer's own error text, so the caller can judge whether re-sending will work. Scoped to `AuthInfo.owner`, never request input. Bounded by window and limit because recall is the hottest authed route; `remember_jobs (owner, status, updated_at DESC)` from migration 006 already covers the predicate and ordering. A lookup failure degrades to "nothing to report" rather than failing the recall it is attached to. Reports repeat until they age out. Suppressing after one sighting would put the burden back on the caller remembering to act, which is the failure mode this removes. `failed_writes` is skipped when empty, so an older client and an older relayer both see exactly today's response.
`memwal_remember_status` advertised `waitMs` up to 60000. The MCP SDK times a request out at `DEFAULT_REQUEST_TIMEOUT_MSEC` — also 60000 — unless the caller overrides it, so a caller using the documented maximum always lost the race: the client gave up first and the agent saw `MCP error -32001: Request timed out` with no way to tell a slow write from a broken tool. Found by driving the real tools against the production relayer rather than a mock: `waitMs: 60000` on a three-job batch reproduced it every time. The ceiling is now 45s, which leaves headroom for the round trip and still covers the median write, and the tool description interpolates the constants instead of restating them, so the advertised range cannot drift from the schema again. A job outliving the wait is not lost — the job_id stays valid and the caller asks again, which is why this tool is separate from the write. Also start the recall failure report concurrently instead of awaiting it after hydration. The published SDK aborts a recall after a hard 15s that no caller can raise, and a live recall was measured landing on exactly 15.0s, so the report has to cost the critical path nothing. Adds a regression test asserting the tool's advertised maximum keeps at least 10s of headroom under the SDK's request deadline, and a live end-to-end script that walks remember → status and bulk → job_ids → blob_ids against a real relayer, since the mocked tests cannot catch a client/tool deadline collision.
…anded The live check demanded a blob_id for every job in a 45s window. Measured write latency is p50 ~34s and p90 ~65s, so that window legitimately expires with writes still in flight — the check was asserting the relayer be fast rather than the tool be correct. What must hold is that every job comes back with a definite state, that the count of blob_ids matches the count reported saved, and that a report with nothing saved says so rather than reading as success.
…cepts The bridge carries its own copy of the tool list to answer tools/list before the relayer session is up. Two entries had drifted from the sidecar and both break a real flow during that window. memwal_remember_status advertised only `job_id`, marked it required, and set additionalProperties:false — while the sidecar takes `job_id` OR `job_ids`, and the pending body memwal_remember_bulk returns tells the agent to come back with `job_ids=[...]`. A schema-validating client refuses that call, so the instruction the tool itself gives is unfollowable and a batch cannot be settled at all until tools/list_changed lands. memwal_remember_bulk still carried its pre-fast-return description, with no hint that a result can come back ACCEPTED-but-not-saved. An agent reading it reports a queued batch to the user as stored. Neither id field is marked required: the sidecar rejects both-at-once and neither-at-all, which JSON Schema cannot express here without a `oneOf` some clients mishandle, so that stays a handler check. tool-definitions.test.mjs only ever compared the bridge against literals, never against the sidecar — which is why the drift was invisible. It now pins the parts an agent acts on: that a batch can be settled, and that both write tools admit a result may not be saved yet. Verified against the old definitions, where both new tests fail.
Observed against the live relayer while timing the flow: settling a batch printed 4. [still uploading] job_id=2868f48f… error=polling timed out after 45000ms `waitForRememberJobs` stamps that message on every row that had not landed when the budget ran out. It is our clock expiring, not the job failing — the write is still on its way — but rendering it as `error=` next to "still uploading" tells the agent the opposite, and the agent tells the user. Only a terminal row (failed / not_found) explains itself now. Also re-syncs the bridge's advertised waitMs ceiling, which drifted again in the other direction when the sidecar lowered MAX_STATUS_WAIT_MS to 45s. The bridge still advertised 60000, and a caller taking it at its word got MCP error -32602: Number must be less than or equal to 45000 That is the same failure shape as the job_ids drift, so it gets the same treatment: a test pinning the bound rather than a one-off correction.
…lyze writes The sweep I added keyed only on `preparation_encrypted_b64 IS NULL`, on the assumption that column marks "preparation never finished". It does not. It is written in exactly one place — spawn_prepare_remember_job, the SINGLE remember path. `/api/remember/bulk` and `/api/analyze` insert their rows directly and never write it, so for those two the column is ALWAYS NULL, healthy or not. The predicate therefore matched every bulk and analyze job that sat in `pending` past the 10 minute TTL — which is ordinary, not pathological, since WALRUS_UPLOAD_PER_WALLET_CONCURRENCY defaults to 1 and the queue is minutes deep under load. It would have marked live paid writes `failed` while their upload was still queued, and the recall failure report would then have told the user to send them again. Worse than the gap it was meant to close. `prepare_claimed_at IS NOT NULL` is the missing half: only claim_remember_preparation sets it, and only the single path calls it (analyze passes prepare_claim_token: None). Together the two columns mean "this row claimed a preparation slot and never redeemed it", which is the state that actually has nothing left to resume it. Orphaned bulk and analyze preparations stay unswept. That is the pre-existing behaviour, left alone deliberately rather than guessed at: neither path persists anything that separates stranded from queued, so sweeping them needs a durable marker they do not have yet. Two tests seed exactly the row shape those endpoints write — pending, no claim, no preparation, well past the TTL — and assert it survives. Also repairs the build: RecallResponse gained `failed_writes` but two test initializers in routes/recall.rs were not updated, so `cargo test` did not compile on this branch at all (3 errors, present before this commit).
…y is free Two defects with the same shape: a message written for the single-remember path was reused where its guarantees do not hold. SECURITY — the accepted-then-failed report attached to every recall read `remember_jobs.error_msg` straight out of the table. Every other client-facing view of that column (`GET /api/remember/:job_id`, `POST /api/remember/bulk/status`) runs it through `sanitize_job_error_for_client` first, which exists to do two things: replace an infrastructure-funding failure with INFRA_JOB_ERROR_MESSAGE, and redact long hex runs. Skipping it meant a WAL shortfall published the relayer's own hot wallet and its exact balance — "Insufficient balance of 0x356a26…::wal::WAL for owner 0x8d3c1f0a…c0d. Required: 64367730, Available: 10708877" — to every tenant whose write landed on that wallet, on every recall, for 24 hours. It also reads to an agent as "top that address up", which is the precise scam confusion INFRA_JOB_ERROR_MESSAGE was written to prevent. The repo already asserts this cannot happen (infra_wal_balance_failure_hides_relayer_wallet_address); that assertion was simply never extended to this path. Sanitized at the recall boundary rather than in storage, since `routes` is not reachable from the lib. CORRECTNESS — `withAcceptDeadline` emitted one message for every caller, ending "the SDK reuses the same idempotency key, so a retry attaches to the existing job instead of queueing a second paid copy". True for `POST /api/remember`, which carries a content-derived key. False for `POST /api/remember/bulk`, which carries none: the handler mints a fresh uuid per item and inserts with no conflict clause, so a retry is N more paid Walrus blobs for the same N facts. The deadline makes that near-certain rather than merely possible — `withDeadline` deliberately does not cancel the request, so when it fires the relayer has usually accepted already. The advice is now chosen per path, and bulk is told to check `memwal_recall` before re-sending anything. Tests pin both directions, because collapsing the two messages into one is how this happened: bulk must never claim idempotency it does not have, and the single path must keep saying a retry is safe.
`deadlineSignal` unref'd its timer, which reads as tidy and is exactly
wrong for this timer: the deadline is the one thing a caller IS waiting
on. Unref'd, it stops the clock the moment nothing else holds the event
loop open, and the stalled request it exists to bound then hangs forever
— the bug the deadline was added to prevent.
CI caught it as six cancelled tests in request-timeout.test.mjs
("Promise resolution is still pending but the event loop has already
resolved"): the stub fetch is a bare promise with no socket behind it,
so the loop drained and the deadline never fired. A real socket normally
keeps the loop alive, which is why this survived manual testing — but
"normally" is not a bound, and any caller whose transport does not ref
the loop inherits the unbounded hang.
Both call sites already run `dispose()` in a `finally`, so a ref'd timer
cannot outlive its request either.
`a_queued_bulk_job_is_never_swept` and `a_queued_analyze_job_is_never_swept` failed against a sweep that is correct. The helper was the problem: it stamped `prepare_claimed_at` on every seeded row, claim token or not, so a row meant to stand in for a queued bulk write carried the one column the orphan pass keys on and was duly failed. `/api/remember/bulk` and `/api/analyze` insert neither the token nor the timestamp; only the single-write path claims, and it writes both at once. Stamping the timestamp alone is a shape the database never holds, so the two tests were asserting against a fiction while the sweep they guard went unexercised. Seed the timestamp only alongside a token. Every row that must be swept already passes one, so the orphan-preparation tests are unaffected. Verified by `cargo check --tests`; the DB tests themselves need a pgvector Postgres, which CI has and this machine does not.
The cold-start tool list gained `memwal_remember_status` so a client that keeps the first `tools/list` can resolve the job ids `memwal_remember` and `memwal_remember_bulk` now return. The login-handoff test pins that list exactly, and was never updated, so it failed on the tool being present rather than on anything being wrong. Title and annotations are copied from the server definition (read-only, non-destructive), so the assertion keeps pinning the metadata clients receive rather than just the name.
Observed live against production while benchmarking: once the per-delegate-key
budget (60 weighted requests/minute) was spent, memwal_remember failed four
times in a row with
Tool error: Walrus Memory server error (429): {"error":"Rate limit exceeded",
"layer":"delegate_key","limit":"60 weighted-requests/min","retry_after_seconds":60}
and the facts were never written. Nothing retried, nothing honoured the
advertised cooldown, and nothing told the user a memory had been dropped. That
is the quietest failure this system has.
Fast-return makes it likelier rather than rarer: settling a batch adds requests
on top of the write, so an agent saving several facts in one turn spends the
budget faster than one that blocked.
A short cooldown is now absorbed (503 AUTH_UPSTREAM_UNAVAILABLE advises ~5s),
and a long one is reported. Sleeping out a 60s cooldown inside a tool call
would just be the hang this branch exists to remove, and the MCP client would
time out first — so the message names the wait, states plainly that the fact
was NOT saved, and points at the cheaper shape: one memwal_remember_bulk
instead of N memwal_remember calls, one memwal_remember_status(job_ids) instead
of N status calls.
Only rejections that provably never reached the handler are retried — 429 from
the limiter, AUTH_UPSTREAM_UNAVAILABLE from the delegate-key lookup. A 500 is
left alone: it could have been thrown after a write started, and
/api/remember/bulk has no idempotency key, so retrying it would store every
fact twice.
The absorb budget lives inside the accept deadline (8s of 15s) because
withAcceptDeadline wraps this; a test pins that ordering, since growing the
budget past the deadline would turn every absorbed retry into a spurious
"did not accept".
… claim TTL `memwal_remember` tells an agent to send a failed fact again. The derived idempotency key collapses that retry onto the failed row, and `claim_remember_preparation` refused the claim for 60s — while the handler answered 202 ACCEPTED regardless. The caller was told the write was durably queued while nothing at all was running, which is the one thing this branch's whole pending-vs-saved contract exists to prevent. The TTL protects a preparation that is still RUNNING. A job that reached `failed` has none: its preparation either errored on its own or the stale sweeper failed it and cleared its token. So `status = 'failed'` now bypasses the age check. That is safe because fencing is done by the token, not the clock. A claim rotates `prepare_claim_token`, and a straggler's own write is `WHERE ... prepare_claim_token = <old>`, so it matches zero rows and returns before `enqueue_wallet_job` — it cannot mint a paid blob for a job someone else has re-prepared. The test drives exactly that: re-claim a just-failed job, then watch the previous token's UPDATE affect nothing. Second half: stop answering "pending" when no claim was taken. Losing the race now means a concurrent retry won it, so the handler re-reads and reports what that winner actually left behind instead of asserting a state it never reached. A companion test pins that the TTL still does its real job — a `pending` row claimed a moment ago keeps its claim.
analyze was the last tool still blocking to terminal, which left it the slowest in the set by a wide margin: 37.0s measured against dev in the same session where memwal_remember had dropped to 0.2s there. Its wait has the same shape as bulk's — N Walrus writes, one upload per wallet — so there was no reason for the answer to be shaped differently. Extraction is still waited for. `analyze()` resolves once the LLM has run, so the facts it found lead the reply, which is the half an agent can act on straight away. Only the upload of those facts is handed back as job_ids, each paired with its fact so a later partial failure is actionable. Text that yields nothing says "Extracted 0 facts — nothing was saved" rather than handing back an empty batch and a status tool to call about it. 6 new tests. Suite: 302 run, 269 pass, 33 fail — the same 33 sandbox port-binding failures present on dev. tsc --noEmit clean.
…-lookup-to-chat fix(chatbot): scope the vote existence check to the chat (WALM-657)
…(WALM-648) Holding the deadline across the body read put the non-2xx block inside the `try`, and `timedOut()` is a sticky flag — so once the timer had fired the catch replaced whatever was thrown, including a fully classified 429 carrying `status`, `serverCode`, `retryAfterSeconds` and `cause`, with a generic 504. `retryAfterDelayMs` reads exactly `retryAfterSeconds` and `cause`, so the polling loop then fell back to its own curve and re-tripped the relayer's rate limiter — the starvation #921 fixed. Measured against a 429 whose body lands after the poll deadline: 426ms between polls instead of the stated 1s. Convert to a timeout only when the error carries no status of its own. A genuine abort mid-read has none, so a stalled body still surfaces as the bounded 504 this branch added.
…cross-body fix(sdk): keep the request deadline armed across the body read (WALM-648)
…-with-a-version into harryphan/walm-643-codex-installer-deletes-unrelated-hooks-with-matching Both branches rewrite install_codex_hooks.mjs. WALM-640 replaces the Codex MCP registration (npx -> node + absolute launch_mcp.mjs, migrating an existing npx block); WALM-643 replaces the filename-substring ownership heuristic with the _memwal marker plus exact command identity and filters hooks individually. The two touch disjoint parts of the file, so the only textual conflict was the header docblock, resolved to state both.
…ain-pending-jobs fix(python-sdk): keep unreturned jobs pending across bulk status polls (WALM-660)
…n-a-projects-fake-mcp-package-even-with-a-version fix(mcp): launch the MCP server from a trusted absolute path (WALM-640)
…aller-deletes-unrelated-hooks-with-matching fix(mcp): remove only MemWal's own codex hook entries (WALM-643)
…memory-lacks-consistent-secret-filtering-and-user fix(mcp,server): one automatic-memory policy with write-side secret redaction (WALM-642)
Merge dev into staging
* feat(sdk): send recall's deadline so the relayer can say where it stalled (WALM-396) recall() now puts deadline_ms (15000, the same number it aborts at) in the signed body. A relayer that reads it answers a recall about to miss that deadline with a 504 RECALL_TIMEOUT naming the stuck step. Older relayers ignore the unknown field, so deploy order does not matter; a header would have needed the CORS allow-list first. * feat(server): answer a recall about to miss its caller's deadline with the stuck stage (WALM-396) When a recall carries deadline_ms, the handler runs its pipeline under a budget one second shorter (2s floor, input capped at 600s) and, if that runs out, answers 504 RECALL_TIMEOUT naming the stage it was in: embed, vector_search, walrus_download or seal_decrypt. The stage lives in a task-local marker, so fetch_batch keeps its signature and analyze, which shares it, is unaffected. Callers that send no deadline (Python, older SDKs) run to completion as before. For every caller, a recall dropped before it finishes logs the stage it was in, which is where a caller hanging up used to leave nothing. * fix(mcp): name the cause of a timed-out tool call and check relayer health (WALM-396) A recall the SDK gave up on reached the agent as 'Tool error: This operation was aborted'. wrapTool now sorts failures: a relayer RECALL_TIMEOUT names the stuck step with advice for it; an SDK timeout or a failed connect probes the relayer's /health (2s) and reports the result. Each message carries Cause / Relayer health / Next step. Writes are never told a retry is safe. Every other error keeps its old wording. * fix(mcp): check relayer health before answering a call whose reply was lost (WALM-396) A sent call past its deadline used to be answered 'did not answer this call ... safe to retry', which cannot tell a dead relayer from a wrong URL from one stuck call. The sweeper now asks the relayer's /health first (3s, MEMWAL_MCP_HEALTH_PROBE_MS) and answers with Cause / Relayer health / Next step; writes keep their no-blind-retry text with the health line added. Bookkeeping stays synchronous: the answer is written only if the entry is still the one in flight when the probe settles, so a late reply, a logout or a shutdown that answers it meanwhile wins. memwal_recall gets its own 90s ceiling, so a lost recall reply is answered at 2 minutes, not 4. * fix(mcp,server): harden the health probe and stop blaming a healthy relayer (WALM-396) Review follow-ups: - A non-integer or huge MEMWAL_MCP_HEALTH_PROBE_MS made AbortSignal.timeout throw outside the probe's try, and the sweeper's chain had no catch: the first lost reply crashed the bridge. The signal is now built inside the try (bridge and sidecar), the value is floored and capped at 60s, and the sweeper answers even if the probe rejects. - On SDK 0.1.7 the SEAL session is built on the Sui fullnode before the recall request, under the same 15s clock. A failure or stall there was reported as the relayer's. The relayer is now named only when its own /health also fails; otherwise the message says the relayer answered and names the host that failed when Node reports it. - The recall deadline is measured from request arrival, so auth and rate limiting come out of the caller's budget rather than the 1s margin. - The bridge probes once per sweep, not once per expired call, and writes nothing after stdin has closed. A panic no longer logs as a hang-up. - /health's write_ready=false and writes=paused show in the health line. - 'This call only reads' becomes 'cannot store a duplicate', which is also true of memwal_restore. * fix(server,mcp): answer a spent deadline at once and keep the failed-write report off it (WALM-396) Review follow-ups from #931: - The 2s floor applied after subtracting time already spent, so a recall whose deadline went on auth still ran 2s and answered a caller that had given up. budget_for now returns Unbounded | Run | Exhausted: the floor applies to the caller's deadline only, and a spent deadline is answered at once with stage 'auth'. - The failed-write report was awaited inside the deadline, so a stalled courtesy lookup could discard a finished recall and blame the last stage. recall joins it after the pipeline, bounded by the same deadline. - The bridge's answering callback had no catch; a throw left the call marked probing and unanswered. It now clears probing so the next sweep answers. - 'safe to retry once that is resolved' read as 'retry once'. - Code comments keep a one-line why; ticket ids and history stay in the changelog. * fix(mcp,sdk): keep a call being answered off the replay, and give the 504 room (WALM-396) The sweeper marks a sent call whose reply is lost, then answers it once `/health` comes back — but left the entry in `inFlight` for the whole probe. A reconnect landing in that window replayed it, so a write with no idempotency key ran a second time while the agent was told it never ran. The entry is now flagged `orphaned` when the sweeper takes it, and `reconnect()` skips those; the flag is never cleared, so the path where the answer itself throws cannot let a replay back in either. `recall()` sends `deadline_ms: 14000` against its own 15s abort, derived from it so the two cannot drift. The relayer's 1s margin runs from request arrival, so it pays for the reply's trip back but not for the connect the SDK's timer has already been counting — on a cold TLS handshake the 504 landed after the abort it exists to beat. --------- Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com>
ducnmm
approved these changes
Sep 22, 2026
Align SECURITY.md with MystenLabs/walrus (email security@mystenlabs.com). Retarget issue templates from /security/advisories/new to /security/policy.
docs: add security policy and fix vulnerability-reporting 404
docs: fix Claude OAuth connector and Code setup guidance
Promote dev → staging
3 tasks
This branch was successfully 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.
Takes current
stagingontomain(post–#922 / post–WALM-617 hop).Heads at open
staging0a183481main0ffbacbb173 commits on
stagingnot inmain, 167 files, +21245 / −996. The 10 commits onmainnot instagingare older promotion merge commits only (merge-basecbbc9c62= tip of #916).Gate evidence
CI on
0a183481: 28 success, 1 skipped, 0 failures. The skip isE2E / dev relayer(dev-only).Staging tip includes #922 (merge of
dev, with #921 migration wiring) plus the security / MCP / remember-latency batch that landed ondevafter #909.What this promotes (high level)
Write path / MCP latency (#918, #921, follow-ups)
memwal_remember/ bulk / analyze return withjob_id; new status tooling; deadline across body (WALM-648); orphan / failure-report correctness.Security / plugin surface (#958–#963)
mcpPackageVersion: 0.0.14-dev.2inpackages/mcp/plugin/plugin.json(published on npm);packages/mcpversion field is0.0.14.SDK / Python / other
0.1.8(main0.1.7), MCP package0.0.14(main0.0.13), plus WALM-646/647/649/656–660/663/665/666 and related fixes merged via Merge dev into staging #922.Package / npm release ordering — read before merging
npm today:
0.0.14/0.1.8are not onlatestyet. Merging this runsRelease MCP Package/ SDK release onmainand is expected to publish stables from the in-tree version fields (no pending changesets under.changeset/besides config).Watch after merge: confirm
npm view @mysten-incubation/memwal-mcp@0.0.14 versionand SDK0.1.8resolve before announcing plugin/latest. The in-repo plugin launcher pin remains0.0.14-dev.2until a follow-up bumps it to0.0.14(CI currently requires the pin to exist on npm —0.0.14-dev.2does).Not in this promotion
dev, not onstaging.dev, not onstaging.Supersedes
#907 (WALM-626 Redis reconnect hotfix on
main) — content already on the line via #906 / staging history. Close #907 after this merges rather than merging both.Suggested merge gate
0a183481and smoke still green.0.0.14/0.1.8.