release: promote dev to staging (post-#751 delta) - #789
Closed
harrymove-ctrl wants to merge 80 commits into
Closed
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]
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
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.
Collaborator
Author
|
Closing per Harry: promotion should go through the dev branch directly. |
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.
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.