Skip to content

release: promote dev to staging (post-#751 delta) - #789

Closed
harrymove-ctrl wants to merge 80 commits into
stagingfrom
release/staging-sync
Closed

release: promote dev to staging (post-#751 delta)#789
harrymove-ctrl wants to merge 80 commits into
stagingfrom
release/staging-sync

Conversation

@harrymove-ctrl

Copy link
Copy Markdown
Collaborator

Replaces #788. That PR listed 79 stale commits because #751 was squash-merged (e9fbd82 has one parent), severing the history link between staging and dev; content-wise staging was already current (tree(staging) == tree(dev@e5ae9e06)).

This branch is built from staging's side as one merge commit, so only the real post-#751 delta appears: 27 commits from 8 PRs.

Resulting tree is byte-identical to origin/dev tip 5e1e16f (empty git diff origin/dev).

Merge with a merge commit, not squash, otherwise the history link is severed again and the next promotion re-conflicts.

hien-p and others added 30 commits August 14, 2026 12:40
…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]
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
ducnmm and others added 28 commits August 22, 2026 23:32
Send encrypted_data on lightweight rememberManual, register POST /api/embed,
decode SEAL ids with hexToBytes, reject whitespace in Python hex_to_bytes,
and add the documented env=dev preset.

@mysten-incubation/memwal 0.1.5
memwal 0.1.8
charge_explicit_weight used window_start=now, so Redis pruned in-window
history after /api/analyze. Charge now shares the live limiter windows.
GET /health stays HTTP 200 and adds write_ready from a short cached
sidecar probe. Seal transport errors no longer leak loopback URLs.
…idge

Overlapping tools/call POSTs dropped the SSE session and left later
calls hung until restart. Outbound POSTs are serialized; stale queued
posts are skipped after reconnect. Login timeout warns that an on-chain
key may already exist.
recall() now exposes relayer dropped_count. health() accepts optional
write_ready. HTTP and remember-job errors strip localhost URLs so a
write-path outage does not send callers looking at their own machine.
Dashboard lists owner namespaces from the read API so named namespaces
are no longer invisible. Playground keeps the last namespace per
account and accepts a one-shot ?namespace= link. Connect MCP fail copy
tells the user an on-chain key may already exist.
Sui style guide forbids em dashes in prose.
Sui style guide requires present tense for product behavior.
chore(github): add bug and feature issue forms
…irect

fix(oauth): persist the client loopback redirect URI (GH #619)
…store

fix(sponsor): restore pending binding after sidecar execute failure (GH #617)
fix(mcp): serialize SSE POSTs so concurrent recall cannot hang the bridge
… live rows

Forget now NULLs remember_jobs.idempotency_key in the same transaction as
the vector delete, so re-analyzing forgotten text is a new write instead
of a silent no-op. Same-text extracts with a different importance bucket
patch the live row rather than minting a second blob.
fix(mcp): fail non-TTY login and single-flight memwal_login
fix(mcp): require a registered delegate before opening a session
…lyze-gas

fix(relayer): restore timeouts, oauth authorize cap, analyze idempotency
The Memory API Latency job on the promote PR failed because 5/10 warm
recalls got a Cloudflare error page, not because /api/recall itself
failed. Retry HTML/5xx/429 flaps; leave auth and contract 4xx fatal.

Also apply the style-guide nits from that PR: onchain, through, active
voice, and an unstacked 0.1.4 changelog heading.
…-sdk-ci

ci: fold the JS SDK suite into test.yml and serialize bench-account jobs
…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
fix(sdk): surface dropped_count and redact loopback URLs
fix(sdk): align remember-manual, embed, hex, and env=dev
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
Real delta since the #751 promotion snapshot (e5ae9e0) is 27 commits from PRs #673 #699 #713 #724 #737 #744 #746 #747. The previous promotion PR listed 79 commits because #751 was squash-merged, severing the history link; this merge is built from staging's side so only the true delta appears. Resulting tree is byte-identical to origin/dev. Also drops test-sdk.yml, deleted on dev in 135b006.
@harrymove-ctrl

Copy link
Copy Markdown
Collaborator Author

Closing per Harry: promotion should go through the dev branch directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants