release: promote staging to main - #796
Conversation
…oncile lookup The whole point of PR #562's idempotency work is: if the sidecar crashes right after minting a paid Walrus blob but before its journal write lands, a retry must find and adopt that orphaned blob instead of minting a second paid one. The lookup that's supposed to find it was searching for the wrong tag. The durable-upload register step (walrus-upload-journal.ts — the only path every real /api/remember call goes through today) tagged the minted blob's on-chain metadata with memwal_migration_job, an unrelated, dead-code constant borrowed from the V1->V2 migration feature (its only reader, findOwnedBlobObjects, has zero callers anywhere in the codebase). The reconcile scan that runs on a lost journal, scanOwnerForJobBlob (walrus-query.ts), searches for memwal_job_id instead. The two could never match, so a crash at exactly the wrong moment would silently mint twice — the precise failure PR #562 exists to prevent. memwal_job_id was already the correct key, used correctly, by the legacy (non-durable) upload path — but that path is dead code today, since every current caller sets remember_job_id: Some(...), which always routes into the durable path instead. Fixes it by introducing a single shared MEMWAL_JOB_TAG_KEY constant in util.ts and having both write sites (durable + legacy) and the read site import it instead of each hardcoding the string literal, so the two sides structurally cannot drift apart again without someone deliberately un-importing the shared constant. New regression test pins the constant's value and greps all three files to confirm none hardcodes a competing 'memwal_job_id' literal. 193/193 sidecar scripts tests pass (7/7 in the directly affected files). tsc --noEmit: no new errors (one pre-existing, unrelated error in mcp/__tests__/integration.test.ts, confirmed present on unmodified dev too).
Found on production (researcher.demo.memwal.ai), then reproduced and A/B'd
locally against the same condition.
getTitleModel() hardcoded "google/gemini-2.0-flash-001", which OpenRouter has
retired — it returns 404 "No endpoints found" for our own production key. Every
other call site already used google/gemini-2.5-flash; this one was missed.
Because titlePromise was awaited with no try/catch inside createUIMessageStream's
execute(), the rejection reached the stream's onError, which injected an
{"type":"error"} part — so the client rendered a fatal chat error even though the
real answer had streamed successfully underneath it.
Production impact when this was written: 6 of 43 chats have a user message and no
assistant reply (4 of them on or after the 2026-08-14 deploy), and 11 of 43 chats
are still titled "New chat".
apps/chatbot already had this guard from 334cf91 and researcher was missed at the
time. This ports it, and also fixes the root cause that commit left in place: the
retired model id itself. Moving it to a TITLE_MODEL constant beside chatModels
keeps it from drifting away from the models we actually support.
Auditing the rest of the picker with the production key turned up two more retired
ids — anthropic/claude-3.5-haiku and anthropic/claude-3.5-sonnet 404 identically.
Three of the six selectable models were dead. Removed, and recorded in the new
unit test so they cannot be reintroduced by copy-paste.
Verified locally (docker pgvector + next dev, real OpenRouter key, PLAYWRIGHT
unset so getTitleModel is not swapped for the mock):
- with the guard, retired title model: answer streams, no error frame
- without the guard, retired title model: answer streams, error frame injected
- with the production key: title generates for real, no error frame
- pnpm test:unit 11/11; tsc --noEmit clean on the changed files
Not addressed here: production also shows chats with no persisted assistant row at
all, which did not reproduce locally — the assistant message persisted in both arms
of the A/B. That may be the older deployed build or the unhandledRejection aborting
before onFinish, and is worth re-checking once this ships. apps/chatbot still
carries the same retired model ids.
Two UI defects on the empty-chat screen. Tooltips rendered as a ~2px sliver under their trigger. Not overflow clipping — every ancestor was overflow:visible. TooltipContent was not wrapped in TooltipPrimitive.Portal, so it stayed inside the chat header, and `position: sticky` always creates a stacking context. The content's z-50 therefore only applied within that 50px-tall header, and the part of the tooltip below 50px painted underneath the page content that follows the header in DOM order. Only the overlapping sliver was visible. Wrapping in Portal moves it to the body and out of the header's stacking context, matching what noter's tooltip.tsx already does. Affects every tooltip in the app, not just the header ones. Suggestion pills wrapped to two lines when the text was long, making one pill taller than the rest. The pills now hold a single line at a fixed 42px (matching the loading skeleton) and truncate with an ellipsis, with the full string in `title` — sprint-generated suggestions are arbitrary length, so the copy change alone would not have held. Shortening the default prompts also surfaced a trap: DEFAULT_SUGGESTIONS was declared twice, byte-identical, in suggested-actions.tsx and use-sprint-greeting. Only the hook's copy is reachable, because useSprintGreeting always supplies the fallback through the sprintSuggestions prop — editing the component's copy changed nothing on screen. Collapsed to one exported constant. Verified in the browser at each step: tooltip renders in full on both the sidebar toggle and My Stuff; all four pills sit on one line at equal height. tsc --noEmit clean on the changed files; pnpm test:unit 11/11. apps/chatbot has the same unportaled tooltip.
Reopening a chat whose last message is from the user threw a client
TypeError: Cannot read properties of undefined (reading 'state'), from
AbstractChat.resumeStream in AI SDK 6.0.37.
Root cause (diagnosed via live repro on a chat with one user message and no
assistant reply): chat/[id]/page.tsx enabled autoResume, so useAutoResume
called resumeStream() on mount — but resumable streaming was never
implemented; api/chat/[id]/stream/route.ts is a stub that always returns
204. Overlapping resume attempts (easy under dev double-invoked effects, but
not inherently dev-only) race inside the SDK: one clears activeResponse
while the other reads its .state.
Two changes:
- autoResume={false} on the chat page — honest, since there is nothing to
resume until the stream route is real.
- useAutoResume now guards with a ref so at most one resume attempt fires.
The hook's "we intentionally run this once" comment was aspiration, not
enforcement — the dep array does not guarantee it.
This path matters beyond dev: production has 6 chats with a user message and
no assistant reply (fallout tracked in c031c47), and reopening any of them
walks this exact code path.
Verified: the repro chat now loads with a clean console (previously threw on
every load). tsc --noEmit clean; pnpm test:unit 11/11.
app/(chat)/layout.tsx loaded pyodide from a CDN with strategy "beforeInteractive" — a leftover from the Vercel ai-chatbot template's Python code-runner. Nothing in this app calls it: there is no loadPyodide/runPython reference anywhere, and no code-execution feature. It cost a multi-MB script download on every chat page for nothing. As of Next 16 it is also an error, not just waste: beforeInteractive inside a nested layout renders a raw <script> tag in the React tree, and every page under (chat) logs "Encountered a script tag while rendering React component" (surfaced right after login, at Layout in app/(chat)/layout.tsx:13). Removing the tag fixes the error and the dead download in one move. Verified: chat loads with a clean console after login; tsc --noEmit clean. apps/chatbot has the same tag in its (chat)/layout.tsx — but chatbot may actually use pyodide for its code artifacts, so it needs its own check rather than a blind copy of this deletion.
…page title
Saved sprints were read-only in the narrow My Stuff panel — no way to copy or
export a report short of drag-selecting the text.
- Copy / Download / Expand actions in the sprint detail header. Copy puts the
full report on the clipboard as markdown (toast-confirmed); Download saves it
as <title-slug>.md. Both go through one pure helper, buildSprintMarkdown
(title + summary + report + References), so clipboard and file are
byte-identical. Helper is unit-tested (3 tests, repo test:unit style).
- Expand opens the sprint in a wide centered dialog with the same
Report/Citations/Sources tabs; the tabbed body is extracted into an internal
SprintBody component rendered by both the panel and the dialog.
onOpenAutoFocus is suppressed so the Copy button's tooltip doesn't pop over
the title when the dialog opens.
- Page metadata was still the template's ("Next.js Chatbot Template",
metadataBase chat.vercel.ai) — now "Researcher | Walrus Memory" with the
real domain, on the local branch and prod alike.
Verified in the browser against a real saved sprint: copy toast + clipboard
content (starts "# <title>", full markdown), expanded dialog opens/closes
clean, tab title updated. tsc --noEmit clean; pnpm test:unit 14/14.
Mirrors the Python SDK's tests/test_integration.py: no-auth health, compatibility and auth-rejection checks that always run, plus authenticated remember/recall/analyze/restore coverage (and the JS-only bulk, manual-mode and embed surfaces) that skips without MEMWAL_PRIVATE_KEY / MEMWAL_ACCOUNT_ID. Authenticated writes land in a per-run sdk-e2e-<random> namespace so the shared bench account's real namespaces stay clean, and remember waits get 120s of headroom over the SDK's 60s default (a live write measures ~44s on dev). Named *.e2e.mjs so the offline unit glob (test/**/*.test.mjs) never picks the suite up; run it via the new test:e2e script. [WALM-353]
Sibling of test-python-sdk.yml. The unit job runs the offline suite on Node 22 and 24 for every PR touching packages/sdk — closing the gap where SDK unit tests never ran in CI at all. The e2e job reuses the benchmark-dev environment credentials, is limited to dev pushes, manual dispatch and a weekly cron (PRs are excluded: fork PRs get no environment secrets, and every authenticated run writes real memories), fails loudly when credentials are missing instead of green-skipping, and uploads a junit artifact plus a run summary. The cron sits 30 minutes after the Python suite's slot so the two weekly runs don't write through the shared bench account at the same time. [WALM-353]
Review of the authenticated half — which cannot run locally without the bench credentials — caught two tests that were guaranteed to go red on the first CI run: - embed() POSTs /api/embed, which is absent from the relayer's protected route table entirely. - rememberManual() sends blob_id, but RememberManualRequest requires encrypted_data and has no blob_id field, so axum 422s the call before the handler sees it. Both methods are covered only by MemWalMock today, which is why the drift went unnoticed. The suite documents the omission inline rather than deleting it silently; the SDK bug is tracked in WALM-371. Also hardens the remaining live tests: the analyze fan-out gets twice the single-write budget (N facts, one wallet-job pipeline each, on a contended shared account), the one-shot status probe tolerates a background 'failed' since it asserts acceptance rather than pipeline health, and MEMWAL_REMEMBER_TIMEOUT_MS now rejects unparseable input instead of silently becoming NaN. [WALM-353]
Researcher had no automated tests — the app with the only real Walrus writes was verified entirely by hand. This adds a 15-test Playwright suite and wires it into CI as `researcher-e2e`, mirroring the existing `chatbot-e2e` job. The suite's reason for existing is the first spec: the retired title-model P0 (c031c47) shipped because nothing would have caught it. `chat-stream.test.ts` now asserts the raw SSE frames directly — a failing title model must not put an `error` frame on the stream. Verified as a real regression test, not just a passing one: with the try/catch guard removed the spec fails with the exact production symptom, `{"type":"error","errorText":"Oops, an error occurred!"}`, while the answer still streams underneath. Four mock seams, all keyed off the existing `isTestEnvironment`, so a run never reaches OpenRouter, Sui, or the relayer: - `providers.ts` — picker model ids now map onto the three registered mock models. The pre-existing test branch passed the raw id straight through, so `languageModel("google/gemini-2.5-flash")` would have thrown NoSuchModelError on the first message; the branch had simply never run. - `models.mock.ts` — a `FAIL_TITLE_GENERATION` sentinel in a user message makes the title model reject, reproducing the P0 on demand. - `delegate-account.mock.ts` — fabricates the `MemWalAccount` object for two fixture identities. The real binding validation still runs, so unregistered keys and unknown accounts fail exactly as they do on-chain (both asserted). - `memwal.ts` — `MemWalMock` for the Walrus client, one instance per process, so CI can never write to the production relayer. Coverage: delegate-key login (form and rejections), streamed replies, reload persistence with zero uncaught page errors (guards the auto-resume TypeError from 39e102a), SSE frame protocol, and the Private/Public boundary across two real accounts. Two behaviors pinned as they actually are rather than as expected: a private chat opened by another user renders Next's not-found page with HTTP 200, because `<Suspense>` flushes the shell before `notFound()` runs; and anonymous visitors are redirected to /login even for public chats, since the auth check precedes the visibility check. Neither leaks content. The second is the known "Anyone with the link" product gap. `global-setup.ts` clears the auth limiter's Redis keys before each run. The limiter allows 10 verify attempts per IP per minute and a run spends five, so without it a second run inside a minute fails at sign-in with a 429 that looks nothing like the real problem. Both identities also sign in once in a setup project rather than per-test, which keeps CI retries clear of the limit. Verified locally: 15/15 green on three consecutive runs (~10s each), from cold with Playwright managing its own webServer and with AUTH_SECRET and the package id unset, so the config defaults used by CI are exercised.
The dev-only restriction was inherited from test-python-sdk.yml, whose comment justified it as 'staging and production follow once their credentials exist'. That premise no longer holds: benchmark-staging and benchmark-production both carry BENCH_DELEGATE_KEY, BENCH_ACCOUNT_ID and BENCH_SERVER_URL today. Each long-lived branch now tests the deployment it corresponds to — dev, staging, main -> production — and workflow_dispatch takes an explicit environment choice, following the selection pattern benchmark-live.yml already uses. Scheduled runs stay on dev, since cron fires on the default branch, so production is only reached by an explicit main push or a deliberate dispatch. Drops the hardcoded relayer URL fallback. With one environment a literal default guarded against an unset variable; with three it becomes the hazard it was meant to prevent, since a production run with a missing variable would silently exercise dev. The credential check now requires MEMWAL_SERVER_URL and fails the job without it. The suite is safe to point at production because every authenticated write is confined to a per-run sdk-e2e-<random> namespace; the only reference to 'default' is the body of the 401-rejection tests, which never write. [WALM-353]
…n account Credentials lived in one global `~/.memwal/credentials.json`. Signing in from one project repointed every other project on the machine at a different account and delegate key, with the `label` field as the only visible signal and no warning at the point of use. Memories written in that state land on the wrong account, on immutable storage, with no delete path. A `.memwal/credentials.json` in the working directory now takes precedence over the global file, the way `.npmrc` and `.git/config` resolve. Presence-based on purpose: creating the local file is the opt-in, so a machine without one behaves exactly as before. Paths resolve per call rather than at module load, since the working directory is not knowable at import time. Replacing a DIFFERENT account now copies the outgoing file aside and reports both ids — the account replaced and the one now in use. There was no backup of any kind before, so an overwrite was unrecoverable. Same-account re-saves (a label change, a rotated delegate) are left alone rather than churning a backup per login. `saveCreds` returns what it did instead of printing directly: the persistence layer should not own user-facing output, and returning it keeps the behaviour testable without capturing stdout. Note on the recorded decision to refuse a different-account replacement unless forced: the incoming `accountId` only arrives in the browser callback, after the user approved and the delegate key was already registered on-chain. Refusing there would discard a registration that cost gas and already grants access, so this warns and backs up instead. A pre-approval notice belongs earlier in the flow, where the outgoing account and target path are already known. Refs #628 (WALM-361).
…age lying Two gaps in the previous commit, both found by asking what was actually verified rather than what was written. The warning itself had no test. The unit tests asserted what `saveCreds` returns, not that anything is ever shown — so the user-facing half of the fix, which is the whole point of GH #628, rested on inspection. Extracted `formatReplacementNotice` as a pure function and covered both arms: a replacement names the outgoing account, the incoming one and the backup, and a first sign-in or same-account re-save stays quiet. The browser success page hardcoded "Credentials saved to ~/.memwal/credentials.json". Now that a project-local file can be the destination, that sentence was capable of naming a file the login had not touched — telling a user the wrong credential path is worse than telling them nothing, especially in the exact scenario this ticket exists to fix. It now reports the path actually written. Also verified with the real binary, which the unit tests do not cover: two projects sharing one HOME, one with a local `.memwal/` and one without, load different accounts. Same command, same environment, only the working directory differs. Refs #628 (WALM-361).
Running the live check against the real binary showed the replacement warning never fired from `memwal-mcp login`, and no backup was written — while the unit tests were green. `login` called `clearCreds()` up front, purely so the `loadCreds()` below would return null and the sign-in flow would run. By the time `saveCreds` executed there was no file left to compare against, so the account change was invisible and there was nothing to back up. The destructive part was never needed: forcing a fresh sign-in means ignoring what is on disk, not deleting it. `login` now passes null instead, so the old file survives until a successful sign-in replaces it. That also closes a data-loss path that predates this ticket. Anyone who ran `memwal-mcp login` and then abandoned the browser flow, or whose login timed out or failed, was left with no credentials at all and nothing to recover from — the delegate key on disk was gone before the new one was ever issued. Adds the live check that caught it (`.live.mjs`, outside the `npm test` glob). It drives the real login process end to end — its own listener, preflight, callback parsing and save — and asserts on what the process actually printed. The browser half is driven programmatically, so no wallet approval and no on-chain registration; the replaced credentials are seeded locally, which is all the account comparison keys off. 10/10 pass. Refs #628 (WALM-361).
… step Completes the two acceptance criteria left open on WALM-361. Warn ahead of approval, not only after. The incoming account is unknown until the callback arrives, by which point a delegate key is already registered on-chain — so a warning that waits for both ids arrives too late to act on. The browser step is the last moment the user can back out for free, so the account currently saved and the file at risk are named before it opens. The after-the-fact notice naming both ids stays; the two answer different questions. Document the resolution order, migration, backup and recovery in the MCP reference: how the two locations resolve, how to scope a project to its own account, that migration is a no-op because creating the local file is the opt-in, what a same-account versus different-account sign-in does, and how to restore from a backup. Existing sections that hardcoded ~/.memwal — first-run behaviour, the credential-file section, memwal_logout, and two CLI table rows — now point at the resolved location instead of asserting the global one. Also notes that backups are never pruned and each holds a delegate private key, and that `.memwal/` belongs in .gitignore. Refs #628 (WALM-361).
The comment claimed the realpath call was a macOS concern. It is not: cwd and homedir both report resolved paths on every platform, and the sandbox already sets HOME and USERPROFILE together, which is what os.homedir() needs on Windows and POSIX respectively. Nothing in these tests is POSIX-only — the reason they are unproven on Windows is that the repository has no Windows runner.
WALM-378: sqlx takes a session-scoped pg_advisory_lock for migrations. Through Neon's transaction-mode pooler that lock (and Apalis's set_config session GUCs leak onto reused backends. The next boot waits 15s, times out, and panics — taking the relayer down until a lucky redeploy. Run sqlx/Apalis migrations on the direct compute endpoint, retry lock contention instead of .expect(), and stop setting session GUCs on the pooled Apalis connections. EOF )
A leftover pooled session can still block the first boot after this fix: advisory locks are database-wide, so the direct migrator waits on the same key. On lock timeout, log the holder and pg_terminate_backend only if that backend is idle (the WALM-378 shape). Also include the direct host in migrate connect errors.
lock_timeout aborts the in-flight migrate statement. If sqlx had a transaction open, later pg_locks / pg_terminate_backend queries fail with "current transaction is aborted" and the idle holder is never killed. ROLLBACK first, and treat idle-in-transaction like idle for this lock key only.
Keep the explanation; do not stamp Linear identifiers into runtime strings or source comments.
…the delegate-key error message
sessionAtom was atomWithStorage(..., { getOnInit: false }), so every fresh
page load (a window.location.href redirect after login, or a plain reload of
/note) rendered once with session=null before the async post-mount hydration
from sessionStorage caught up. useAuth's own effect read that stale null on
the same tick, concluded "logged out", and cleared isLoading before the real
session arrived, so /note's guard fired router.replace("/") on an already
authenticated user. getOnInit is safe to flip to true here: nothing renders
`session` directly, every page branches on the separate authAtom instead,
which always starts isLoading:true on both server and client, so there is no
markup for the eager read to mismatch against.
Separately, connectEnoki/connectDelegateKey flipped the global authAtom.isLoading
flag for the duration of their mutation, and app/page.tsx renders
<AuthButtonGroup /> only while !isAuthenticated && !isLoading. That unmounted
the login form mid-submit, taking its local `error` state with it before the
catch block's setError() could run, so an invalid delegate key just dropped
the user back on a silently collapsed form. isLoginPending (from the mutation
hooks themselves) already tracks per-call pending state for the submit
button, so the global flag no longer needs to move during a login attempt.
…cated Sui JSON-RPC Sui's public JSON-RPC fullnodes were deprecated in 2026; fullnode.testnet.sui.io now answers every JSON-RPC call with "Method not found ... migrate to gRPC or GraphQL endpoints" and no CORS header, which Chrome reports as a generic "blocked by CORS policy" failure. sui-providers.tsx's getJsonRpcFullnodeUrl and enoki-login-card.tsx's useSuiClient() both hit that dead endpoint, so registerEnokiWallets failed on mount ([enoki-login] Setup failed: TypeError: Failed to fetch) and Google sign-in never got past the landing page. dapp-kit's SuiClientProvider is still hard-typed to SuiJsonRpcClient even in the latest published version (1.1.17), so it can't be pointed at a gRPC client directly. Enoki's own `client` option and Transaction.build()'s `client` option both accept the broader ClientWithCoreApi interface instead, which SuiGrpcClient satisfies, so this bypasses SuiClientProvider only where it was actually blocking things: registerEnokiWallets now gets a standalone SuiGrpcClient (lib/sui/grpc-client.ts) instead of useSuiClientContext()'s client, and enoki-login-card.tsx's on-chain reads (registry lookup, dynamic field, transaction/event fetch) move to the gRPC client's include/mask-based API. SuiClientProvider itself stays in place for WalletProvider's wallet-standard connect/sign, which doesn't touch RPC directly. gRPC object/dynamic-field/event reads return raw BCS bytes instead of JSON-RPC's parsed `.fields`, so lib/sui/account-bcs.ts adds the BCS schemas needed to decode them, verified by decoding a live testnet account and matching its stored delegate public key, and round-tripping a registry dynamic-field lookup back to the same account id.
Noter had zero automated tests; the on-chain registration flow, delegate-key auth, and note CRUD were verified entirely by hand. 22 specs across app shell, auth, note lifecycle, and the memory API contract, running against a real Next.js dev server and a fresh Postgres. Mock seam for the new delegate-account binding check ------------------------------------------------------ connectDelegateKey now calls assertDelegateAccountBinding (a separate, already-merged change), which reads the claimed account off-chain via gRPC and rejects any key that isn't registered in its delegate_keys list — so a random, never-registered key/account pair can no longer reach an authenticated session the way it could before that change landed. Mirroring researcher's PR #680 pattern, delegate-account.ts now branches on lib/constants.ts's isTestEnvironment (set by playwright.config.ts passing PLAYWRIGHT=True to the webServer) and serves a fixture object from delegate-account.mock.ts instead of the gRPC read, so the real validation logic still runs meaningfully: an unknown account or an unregistered key fails the exact same way it would on-chain. Noter authenticates a fresh identity per test rather than reusing two shared identities across a whole run (researcher's approach) — with `workers: 2` and ~15 login call sites, two fixed identities would have concurrent tests collide on each other's notes. delegate-account.mock.ts and fixtures/delegate-key.ts instead generate the same 24-entry deterministic pool independently (index N -> accountId byte N repeated, privateKey byte N+0x40 repeated), and the test fixture hands out a never-yet-used entry per call, interleaved by Playwright's parallelIndex so two worker processes never claim the same one. public_key is stored base64, not hex: the binding check's parser tries fromBase64() before falling back to raw hex, and every 64-char hex string (alphabet 0-9a-f, always length-divisible-by-4) also happens to be valid-but-wrong base64, so a hex value there silently decodes to the wrong bytes instead of ever matching. The memory-write specs assert against the real relayer response for a fixture (unregistered) key, so there's no live-Walrus canary in this suite by design, same as #680 documents for researcher: the real remember -> recall round trip against production Walrus Memory stays a manual check. CI job ------ noter-e2e mirrors chatbot-e2e's shape (Postgres service container, cached Playwright browsers, report/trace upload on failure). noter-checks adds tsc --noEmit and a full `next build` so a type or build regression fails CI even on a change the e2e specs don't happen to cover.
Two separate PRs each added a job named noter-checks to the same workflow file — one from an already-merged auth-hardening change (vitest unit tests), one from this branch (tsc + next build). Neither touched the same lines, so the merge went through cleanly with no conflict markers, but the resulting file had two top-level jobs with the identical key. GitHub Actions rejects that outright: the workflow run failed in 0s with no job output at all, before any check even started, which is why this wasn't caught by the local `tsc`/`next build`/e2e verification — those ran the commands directly, never through the YAML that CI actually parses. python's yaml.safe_load didn't catch it locally either; it silently keeps the last duplicate key rather than erroring, which GitHub's stricter workflow parser does not do. Folded the tsc/build steps into the existing job instead of renaming to avoid the collision — one Noter CI job now covers unit tests, type-checking, and the production build, sharing one checkout/install/SDK-build sequence rather than paying for it twice.
…eprecated JSON-RPC useSignTransaction pulls its client from SuiClientProvider, which is still JSON-RPC (dapp-kit's hook types are hard-wired to it). transaction.toJSON() needs that client to resolve move-call ABIs before handing the transaction to the wallet for signing, and fullnode.testnet.sui.io no longer serves CORS headers on its JSON-RPC endpoint, so the browser blocked every sponsored transaction with what looked like a CORS failure. Pre-serialize with our own gRPC client and pass the resulting string instead: dapp-kit's hook accepts transaction as Transaction | string and skips its own resolution when given a string.
Share one delegate fixture pool between the gRPC mock and Playwright so they cannot drift, parse hex public keys before base64, and fail-closed isTestEnvironment in production. CI now sets PLAYWRIGHT=True, times out browser install at 8m (was hanging the 25m job), and uses an isolated Playwright cache key.
fix(noter): gRPC migration, auth race fixes, e2e suite + CI
fix(sidecar): crash-recovery reconcile searches for the wrong on-chain tag
…-sdk test(sdk): e2e suite and CI workflow against the dev relayer
…lidate-bridge-credentials-on-logout # Conflicts: # packages/mcp/CHANGELOG.md # packages/mcp/src/bridge.ts
… SQLSTATE SET LOCAL in after_connect is a no-op under autocommit, so restore statement_timeout / idle_in_transaction_session_timeout as session GUCs and omit lock_timeout (the leak that aborted sqlx migrate). Bound setup_pool.close() with the same startup timeout. Classify lock contention on Postgres SQLSTATE (55P03 / 40P01) before mapping into AppError, so a Display reword cannot disable retries.
…-lock fix(server): stop leaking sqlx migration locks through the Neon pooler
…at-crash fix(researcher): retired title model broke every new chat — plus tooltip, auto-resume, pyodide, and sprint-export fixes
…bridge-credentials-on-logout fix(mcp): invalidate the live bridge session on memwal_logout
# Conflicts: # .github/workflows/test.yml
fix(sdk): surface dropped_count and redact loopback URLs
fix(sdk): align remember-manual, embed, hex, and env=dev
codex plugin marketplace add only looks at .agents/plugins/marketplace.json, never .codex-plugin/marketplace.json. Promote the plugin+marketplace install in docs and TESTING.md; keep the hooks installer as a fallback for older CLIs.
The required-field cast does not overlap RecallResult. Read dropped_count as unknown and accept it only when it is a number.
fix(app): show namespace counts and persist playground namespace
…ealth fix(relayer): align analyze charge windows and report write_ready
Plugin MCP servers are plugin-scoped, not a [mcp_servers.memwal] block, and codex_hooks=true is a no-op on current CLI (hooks are stable/default-on; the missing step is trusting the plugin's hook definitions via /hooks). Also syncs the quickstart hub row and the fallback installer's own comment.
Drop em dashes and one future-tense verb flagged by the style guide audit.
staging carried only stale merge-backs from earlier dev promotions, so every conflicting hunk was an older ancestor of what dev already has. Resolved all in favour of dev and dropped .github/workflows/test-sdk.yml, which dev deleted when the JS SDK suite was folded into test.yml. Resulting tree is byte-identical to origin/dev.
`body.query.is_empty()` is a byte-length check, so a query of " " passed validation and reached the embeddings API, returning a semantically meaningless vector and a plausible-looking 200 instead of a 400. Trim before the emptiness check at the shared server gate, which every client path (SDK, MCP tool, raw HTTP) funnels through. Mirror it as a fail-fast check on the MCP Zod schema and in recallManual(), which had the same `if (!query)` pattern. Closes #425
chore: restore staging history link on dev to unblock #788
… account Addresses review feedback on #680. Locally Playwright reuses whatever `pnpm dev` is already on the port. Started without PLAYWRIGHT that process runs none of the mock seams, so model calls reach OpenRouter, the delegate-account binding check Sui, and sprint saves the real Walrus relayer. `/ping` now reports the seam as `x-researcher-test-mode` and global setup aborts the run with the fix spelled out when it isn't set. Reuse stays on so `PLAYWRIGHT=True pnpm dev` keeps the fast `--ui` loop. The check also runs in CI, where it asserts the webServer env reached Next. The test MemWal client is now keyed on accountId rather than one process-wide mock, mirroring the real client's per-(key, accountId) construction, so a later remember-as-A / recall-as-B spec cannot pass for the wrong reason.
test(researcher): Playwright e2e suite and CI job
fix(recall): reject whitespace-only queries (WALM-300)
…ugin-install-requires-cloned-repo-ship fix(mcp): Codex plugin install via marketplace, not cloned repo
…ts, docs changelogs, and the verify script package.json and pyproject.toml were bumped without the rest of the manual release set: memwal/__init__.py still said 0.1.7, docs/sdk and docs/python-sdk changelogs lacked the new sections, and scripts/verify-manual-sdk-release.mjs pinned the old versions, so the verify gate failed on dev. Ports the 0.1.5 / 0.1.8 sections from the package CHANGELOGs into the docs changelogs and refreshes their answer summaries.
chore(release): sync TS SDK 0.1.5 and Python SDK 0.1.8 release file set
…nit on a dead JSON-RPC mirror The x-seal-session preflight called JSON-RPC sui_getObject against the suiRpcUrl advertised by GET /config, with no timeout. That URL points at a third-party mirror (rpc-testnet.suiscan.xyz) because JSON-RPC on public fullnodes is sunset, and when the mirror went unresponsive today every client init hung for the full read timeout and the whole e2e suite failed with httpx.ReadTimeout. The preflight now queries the public Sui GraphQL endpoint (graphql.<network>.sui.io) for mainnet/testnet/devnet, keeps the JSON-RPC call only as a fallback for custom or local networks that still serve it, and caps both paths at 10s so an unresponsive endpoint fails fast instead of stalling init. GET /config parsing no longer hard-requires suiRpcUrl since the GraphQL path does not need it. Unit tests mock the GraphQL endpoint; all 136 pass.
fix(python-sdk): read package version via Sui GraphQL, stop hanging init on a dead JSON-RPC mirror
release: promote dev to staging
Style Guide AuditAudited 11 file(s) against the Sui Documentation Style Guide. 3 violation(s) found. All must be fixed before merge.
|
ducnmm
left a comment
There was a problem hiding this comment.
Review — staging → main
Same promotion pattern as #757. Head is 474950a8 (merge of #788), base is main at 904eb040 (#757). Merge-base e9fbd82e. Tree delta: 100 files, +4354 / −510. Mergeable, no conflicts.
Checks
- Required GitHub checks are green on this head (server clippy + e2e, MCP login handoff, SDK unit + live e2e, researcher / noter / chatbot Playwright, docs, Move).
- Railway staging deploys succeeded for relayer, app, chatbot, noter, researcher, otel. Indexer correctly skipped (watched paths unchanged).
- Python
E2E / dev relayeris skipped. #795 (GraphQL package-version preflight, stop hanging init on a dead JSON-RPC mirror) is already in this delta, so the body note that the fix “targets dev” is stale — it ships here. - No
services/server/migrations/*.sqland no new required env vars in the file list. The postgres URL helper only rewrites the existingDATABASE_URLto the direct host for sqlx/Apalis migrate (WALM-378). “No migrations or env changes required” holds.
What actually ships vs the PR body
The body lists 8 PRs / 27 commits. First-parent of staging is a single merge (#788), and the three-dot file list is the whole validated dev → staging bundle, which is larger than those 8:
- Relayer: Neon pooler / sqlx migrate lock (#713), analyze charge windows +
write_ready(#744), restore timeouts / oauth authorize cap / analyze idempotency (#736),/api/embed - MCP 0.0.10 → 0.0.11: logout actually drops the delegate key and invalidates the bridge (#699), session auth (#735), login TTY/single-flight (#738), serialized SSE POSTs (#745), Codex marketplace install (#763)
- SDK 0.1.4 → 0.1.5 / Python 0.1.7 → 0.1.8: remember-manual/embed/hex/env (#737),
dropped_count+ loopback redaction (#747), whitespace recall (#790), GraphQL version preflight (#795) - App: dashboard namespace counts + persisted playground namespace (#746)
- Researcher: retired title-model crash, tooltips, e2e CI (#673 / #680)
That undercount is a changelog issue, not a merge blocker — #788 is the staging evidence. Production-sensitive surface is relayer rate_limit / admin restore / recall / postgres_url plus MCP 0.0.11. Staging Railway health is the run-through.
Versions
Publishing on main will bump @mysten-incubation/memwal 0.1.5, MCP plugin 0.0.11, memwal (Python) 0.1.8. Confirm the Release SDK / Release MCP workflows are the ones you want to fire.
Merge
Approve. Merge with a merge commit, not squash, so the history link between staging and main survives for the next promotion.
Completes today's promotion chain (dev → staging merged in #788 at 474950a). Staging's tree is byte-identical to dev; the delta over the last production promotion (#757) is 8 PRs / 27 commits:
dropped_count, loopback URL redactionwrite_readyhealthNo conflicts with main. No migrations or env changes required.
Known CI caveat: the Python E2E jobs may fail with
httpx.ReadTimeout— the third-party testnet JSON-RPC mirror (rpc-testnet.suiscan.xyz) is unresponsive and the Python SDK preflight still calls it (fix in #795, targets dev). This is an external outage, not a regression in this delta; the mainnet mirror used by prod is healthy.Merge with a merge commit, not squash, so the history link between branches survives for the next promotion.