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).
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.
…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
…t naming a file it did not delete Review on #701 found two ways the project-scoping half recreated GH #628 in a different shape. Resolution checked `process.cwd()` only, while the docs compared it to `.npmrc` and `.git/config`, which walk up. `cd src`, or an MCP host launched below the project root, missed the project file and silently used the global account. It now walks up from the working directory, bounded at the project root (`.git`), the home directory, or the filesystem root. The bound is not cosmetic: an unbounded walk climbs into shared parents, and because the home stop only fires when `homedir()` is a real ancestor, a working directory outside it reaches the actual `~/.memwal/credentials.json`. That surfaced as sse-idle-watchdog picking up the developer's own credentials once HOME was sandboxed. `clearCreds()` was called for its side effect and the message built afterwards from `credsPath()`, which by then resolved to the *next* file down the chain. Logout named a file it had not deleted, and said nothing about the one that takes over on the next run under a possibly different account. It now reports what it removed and what survived, and both callers say so. `handleLocalLogout` had the same bug twice and is fixed here rather than left to break on merge. Docs updated to describe the walk and its bound, and the two em dashes the style-guide audit flagged are gone. Tests: 40 pass (35 before), five new covering subdirectory resolution, the project-root bound, and each clearCreds outcome.
`MemWal.create({ key })` fed the key straight into `hexToBytes`, which
only accepts hex. A Sui `suiprivkey1...` string therefore died at
construction with "hexToBytes: input contains non-hex characters",
before any request went out.
That is what broke the JS SDK e2e job against the dev relayer: all 11
authenticated tests failed while the 7 no-auth ones passed, because the
shared BENCH_DELEGATE_KEY secret is stored in bech32. The Python SDK
reads that same secret and passes, since it normalizes the key first
(`memwal/utils.py` `normalize_private_key`) — and its docstring already
claimed the TypeScript SDK took both forms. It did not. This closes that
gap rather than working around it in CI, because the two forms are both
in circulation: `sui keytool` and wallets hand out bech32, while
`generateDelegateKey()` returns hex.
Port `normalize_private_key` to `utils.ts` and call it everywhere a
user-supplied delegate key is parsed: both client constructors and the
two `delegateKeyTo*` helpers. `account.ts` is untouched — its
`hexToBytes` calls take public keys, where suiprivkey does not apply.
The bech32 decoder is hand-rolled rather than imported from
`@mysten/sui`, for the reason `u64ToLeHex` gives: the core path keeps
`@mysten/*` as peer dependencies so the client works without them, and
the constructor is synchronous so it cannot await a dynamic import. The
Python SDK hand-rolls it for the same reason. Verified against
`@mysten/sui` on 2000 random keys.
Co-authored-by: Le Tien Phat <91601109+Niko1444@users.noreply.github.com>
WALM-359 / GH #532. check_storage_quota read SUM(blob_size_bytes) inside a transaction holding pg_advisory_xact_lock, then committed. Advisory xact locks are transaction scoped, so the lock died on that commit; the quota comparison and the caller's INSERT both ran unprotected. Concurrent requests from one owner all observed the same pre-insert total, all passed, and all inserted. Reproduced against real Postgres before the fix: 17 of 20 admitted on the inline path (+16 MiB over a 1 GiB quota), and 20 of 20 with a 150 ms gap standing in for the Walrus upload (+19 MiB). Replace the check with a reservation recorded inside the locked transaction, so admission commits the intent to write before releasing the lock. Usage is now SUM(vector_entries.blob_size_bytes) plus live reservations, which makes a later request in the same burst see an earlier one's bytes even though no row exists yet. One transaction spanning check and insert would close the inline paths but not the enqueued ones: those admit, queue a wallet job, upload to Walrus, and insert minutes later, and a Postgres transaction cannot be held across that. A reservation table covers both groups on one mechanism. Reservations are keyed by the caller's own id rather than a generated one: enqueued paths use the remember_jobs.id the vector row will carry, so jobs.rs releases from an id it already has and nothing new is threaded through the serialized WalletOperation payloads; inline paths mint a local UUID. Release happens after the row is committed, never before, so the overlap errs toward over-counting rather than leaving a window where neither the row nor the reservation counts. Three layers keep a missed release from stranding quota: explicit release on the success and terminal-failure paths, a periodic reconcile against remember_jobs already in a terminal state, and a 15 minute TTL as the backstop. The failure mode is bounded over-counting, never a permanently unusable account. Also removes the TOCTOU comment that claimed the old advisory lock closed this race. Covers all five call sites: remember(), remember_bulk, remember_manual, and both the benchmark and main analyze paths. The 402 / "Storage quota exceeded" response shape is unchanged. Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com>
* fix: point empty 401s at memwal_login (#696) Unauthenticated 401s were surfacing as "Walrus Memory server error (401): <no message>". Return a short, actionable hint to run memwal_login instead. * fix(sdk): use actionable 401 copy pointing at memwal_login Walrus Memory isn't signed in. Call the memwal_login tool, then retry. * fix(sdk): changelog, 0.1.4 bump, and empty-body-only 401 login hint Manual changelog + dump/version for #696. Empty 401s keep the locked memwal_login copy; non-empty 401s keep AUTH_REJECTED triage. * chore: drop extra changeset and ticket IDs from source Manual changelog + version dump is enough. Comments describe behavior only. * chore(sdk): restore package.json description em dash
…isolation fix(mcp): resolve credentials per project, and stop losing them on re-login
…r Console (#554) * Add design spec for WALM-295/296 memory read API Owner-scoped read API (namespaces/memories/agents) plus per-memory expiry fields, covering both tickets since WALM-296 extends the same memories response WALM-295 creates. * Add updated_at/agent_id/package_id columns and pagination index to vector_entries * Split vector_entries schema migration into per-lock-scope transactions * Add AppError::Forbidden (403) for owner-mismatch responses * Plumb agent_id/package_id into insert_vector across all write paths * Normalize empty agent_id string to NULL in restore's provenance lookup * Carry agent_id/package_id through the FinalizeUploadedBlob recovery path * Add GET /v1/owners/{owner}/namespaces * Add GET /v1/owners/{owner}/memories with keyset pagination * Fix limit query error envelope and add real tie-breaking test for memories pagination * Add GET /v1/owners/{owner}/agents (live on-chain delegate key list) * Document the memory read API contract for Console * Fix final review findings: updated_at conflict bump, URL-safe cursors, namespaces pagination, agents caching, route metrics, auth docs * Fix flaky CI test: serialize concurrent CREATE EXTENSION IF NOT EXISTS vector races in memory_read tests * Add end_epoch/expires_at/expiry_synced_at columns to vector_entries * Add WALRUS_STAKING_POOL_ID config for expiry timestamp conversion * Add Walrus staking-state fetch and epoch-to-timestamp conversion * Fix off-by-one-epoch error in expires_at_from_epoch * Surface end_epoch in the sidecar's walrus upload response * Add end_epoch to WalrusUploadResponse and UploadResult * Plumb end_epoch into insert_vector across all write paths * Change end_epoch from i64 to i32 to match on-chain u32 and INTEGER column * Expose end_epoch/expires_at and derive status on GET /v1/owners/{owner}/memories * Add periodic background sweep to populate per-memory expiry * Harden expiry sweep: index expiry_synced_at, avoid burst catch-up, add regression tests * Document end_epoch/expires_at on the memories endpoint * Anchor expiry formula on current epoch, fix sweep failure ordering, fix synced_at index * Fix overflow panic in expires_at_from_epoch's epoch-delta subtraction * Correct comment on why the epoch-delta multiplication uses saturating_mul * Remove internal design-spec doc from the repo * Remove internal design-spec doc from the repo * feat(WALM-297): owner-scoped bearer token auth for the read API (Phase 1) Console can never call WALM-295's owner-scoped read endpoints under the existing Ed25519 signed-request scheme (auth.rs::verify_signature) because it structurally never holds a delegate key — WALM-298's identity-link flow proves control of an owner address entirely on Console's own side and never grants signing capability. This adds the missing bridge: after Console has proven control of owner Y itself, it authenticates as a trusted client via a single shared service credential (team decision: service credential over mTLS/signed-client-assertion, for lower implementation cost on both sides) and mints a short-lived, owner-scoped bearer token limited to memories.read. - owner_token_auth.rs: HMAC-signed opaque token mint/verify, mirroring the existing security_delete_auth.rs token scheme; a FromRequestParts extractor for token-gated routes. - routes/owner_token.rs: POST /v1/owner-tokens (issuance, gated by a constant-time service-credential check, Sui-address validation, and a MemWalAccount existence check) plus GET /v1/owners/{owner}/_token_probe, a minimal token-gated example route standing in for WALM-295's real handlers, which don't exist on this branch yet (fresh branch off dev). Wiring the OwnerToken extractor into the real namespaces/memories/agents handlers is the small follow-up once this branch merges with WALM-295. - rate_limit.rs: independent per-credential and per-owner issuance budgets, fail-closed on Redis error. - types.rs / routes/remember.rs: new Config fields + config-fixture updates. - docs/api/owner-token-auth.md: full contract for Console, including the four distinct 429/503 response shapes across the two independent rate limiters and two independent unavailability paths, and an explicit note on the shared-secret trust boundary (a leaked credential can mint a memories.read token for any owner — accepted Phase-1 trade-off). Verified end-to-end against a live local build (real HTTP, not just unit tests): credential rejection, cross-owner 403, real expiry, forged tokens with altered permissions/audience correctly rejected (proving the scope check isn't hardcoded), rate-limit trip, and no secret leakage across logs/responses. 321/321 lib tests, 432/432 bin tests, no regressions. * Migrate /agents delegate-key listing to gRPC, matching auth's existing JSON-RPC-sunset migration * Migrate /agents delegate-key listing to gRPC, matching auth's existing JSON-RPC-sunset migration * fix(WALM-297): close review findings — IP rate limit, test coverage, doc drift An adversarial multi-dimension review of commit 9c16e46 confirmed 2 major and 5 minor findings; the 2 major ones are fixed here, along with 3 of the cheap minors. 1. (major) POST /v1/owner-tokens had no throttling on guessing the shared service credential: the credential-gate middleware rejected bad guesses in-process before the per-credential rate limiter ever ran, and that limiter is keyed by the guessed value anyway, so a varying guess got a fresh Redis bucket every time. Added owner_token_ip_rate_limit_middleware (mirrors accounts_rate_limit_middleware/sponsor_rate_limit_middleware's unconditional per-IP layer) as the true outermost layer on this route, independent of credential validity. 2. (major) token_probe's actual authorization logic — owner-match via same_owner and the memories.read permission-scope check, explicitly documented as the copy-paste template WALM-295's real read handlers will use — had zero test coverage despite needing no AppState/DB/ Redis to test. Added 5 unit tests covering the match/mismatch and present/missing-scope/empty-scope cases. 3. (minor) docs/api/owner-token-auth.md's "validation order" bullet had the service-credential check listed after Sui-address-format, when in the real request pipeline the credential gate is the outermost middleware and runs first. Corrected, and documented the new IP rate-limit layer. 4. (minor) expires_at used chrono's plain to_rfc3339(), which always renders the UTC offset as "+00:00" — never matching the "Z"-suffixed example the response's own doc comment and the API doc promised. Switched to to_rfc3339_opts(SecondsFormat::Secs, true). Also replaced the silent "fall back to now()" on timestamp-overflow with a real error, since silently reporting a valid token as already expired is worse than failing loudly. 5. (minor) OWNER_TOKEN_TTL_SECS had no upper bound (only rejected 0), which both defeats the "short-lived" security property the token scheme's threat model rests on and could, for extreme values, push the expires_at computation outside chrono's representable range. Clamped to a new MAX_OWNER_TOKEN_TTL_SECS ceiling (24h). Not fixed here (left as follow-ups, all minor/test-coverage-only, noted in review): OWNER_TOKEN_AUDIENCE not deployment-scoped (relies on OWNER_TOKEN_SECRET differing per environment, an operational convention); no test for duplicate-nonce-still-verifies (intentional per the nonce design note, but undertested); no test pinning the per-owner-vs-per-credential rate-limiter independence; no test pinning find_account_by_owner's DB-error path surfacing as 500 rather than being swallowed. Re-verified after all fixes: cargo check --lib / --bin clean, 321/321 lib tests + 437/437 bin tests pass (up from 432 — 5 new tests), no regressions. * Fix rolling-deploy migration race: set updated_at's default as its own statement in 010, before 011's backfill * Add explicit has_more to namespaces/memories responses; fix doc overclaim on deletion visibility * Fix e2e_test.py size-test assertions to check job_id, not id /api/remember returns {"job_id": ..., "status": ...}. The 64KB and large-size test functions asserted "id" in result, so they always reported failure even when the underlying job completed successfully. * fix(WALM-295): batch migration 011, split read-API rate budget, fix pagination/authz findings Standalone commit of the review-fix round applied on top of c09bdd0, independent of any other in-flight work on this PR — see commit body for full detail on each of the 9 findings this addresses. 1. (critical) Migration 011 was a single unbatched full-table UPDATE — crash-loop risk on real-sized tables (live-confirmed against the dev DB, 113k rows, statement timeout -> rollback -> panic -> Railway restart -> identical doomed retry, forever). Moved the backfill into a Rust loop (db.rs, batches of 5000 rows, each its own committing statement) invoked from VectorDb::new() between migrations 010 and 012; neutered 011's SQL body to a no-op, kept the file for numbering. 2. (major) Migration 012's SET NOT NULL took a full-scan ACCESS EXCLUSIVE lock. Split into 012 (ADD CONSTRAINT ... NOT VALID + VALIDATE CONSTRAINT, SHARE UPDATE EXCLUSIVE only) and new migration 014 (fast SET NOT NULL once the CHECK is validated, then drops it). 3. (major) Migration 013's CREATE INDEX CONCURRENTLY IF NOT EXISTS could leave a permanent INVALID index after an interrupted build, silently degrading every memories-listing query to a sequential scan forever. Added a guard in db.rs that detects an invalid index via pg_index.indisvalid and drops+rebuilds it before 013 runs. 4. (critical) The three new /v1/owners/{owner}/* read routes shared the write-path's 30/min delegate-key rate budget with no dedicated allowance -- live-reproduced this trips 429 under completely normal read-pagination traffic. Gave the read API its own dedicated budget (rate_limit.rs, main.rs router split) separate from the write path. 5. (major) storage/sui.rs's DelegateKeysCache never evicted entries -- unbounded process-lifetime growth. Added a periodic sweep task (main.rs) mirroring the existing evict_expired_delegate_keys pattern. 6. (major) Namespace keyset pagination's (MAX(updated_at), namespace) cursor could deliver an already-returned namespace a second time if it was mutated mid-walk (the same failure mode the cursor was designed to fix, in the opposite direction). Added a snapshot_at boundary (memory_read.rs), mirroring the same fix already applied to the memories cursor. 7. (minor) Owner-path 403 check was a raw case-sensitive string compare. Switched to the existing same_owner canonical-address helper. 8. (minor) Memories keyset pagination could deliver the same memory_id twice within one walk if a row's updated_at was bumped mid-walk (an Apalis job retry re-upserting the same vector_id). Added a snapshot_at boundary so rows mutated after a walk began are excluded until the next fresh walk. 9. (major, docs) docs/api/memory-read-api.md described the old shared rate-limit scheme and a "layer": "per-account-burst" value that was never real. Rewrote the Rate Limiting section to describe the new dedicated budget with real layer names/values. Every fix was verified against a live local build: real Postgres migration pipeline runs (12k+ seeded NULL rows crossing the batch boundary, a genuinely-reproduced interrupted CONCURRENTLY build for the invalid-index case), real HTTP rate-limit trips, and the full test suite (cargo test --lib and --bin, no regressions) -- not just static review. Also includes an OpenAPI 3.0.3 spec for the three read endpoints (services/server/docs/api/memory-read-api.openapi.yaml), written to unblock a Console engineer who was blocked on it. * fix(WALM-297): wire OwnerToken into the real WALM-295 read routes Adds auth::verify_read_api_auth, a combined dispatcher for read_api_routes that accepts either WALM-297's owner-scoped bearer token (Console) or the existing Ed25519 signed-request scheme (SDK/dashboard), additive per docs/api/owner-token-auth.md's stated trust model. Swaps it in for auth::verify_signature on the three real routes; handlers are unchanged since they already enforce owner-matching via the shared AuthInfo. Fixes a rate-limit isolation bug the naive version would have had: the bearer path's synthetic AuthInfo.public_key is prefixed per-owner (ownertoken:{owner}) so read_api_rate_limit_middleware's per-key bucket can't collapse every Console-proxied owner into one shared budget. Adds unit tests for the new dispatch/scope-check helpers and opt-in e2e_test.py coverage for the bearer path (success, owner mismatch, invalid token, Ed25519 regression guard). Updates memory-read-api.md and owner-token-auth.md, which both had drifted to actively say this wiring didn't exist yet. * fix(WALM-295): correct memory-read-api.openapi.yaml against live behavior Addresses PR #554 review findings from Niko (Console team), verified against dev: - NamespacesResponse/MemoriesResponse: has_more was missing from both properties and required, despite the live API always returning it and memory-read-api.md documenting it as the only correct end-of-data signal. - next_cursor's description claimed null means "end of data" — backwards. It's always returned on a non-empty page (including the final page of a traversal); null only appears on an empty page. Confirmed live: draining 48 memories, the last page had a non-null cursor and has_more: false. - MemoryItem was missing end_epoch/expires_at entirely — all of WALM-296, which this branch ships. Documented that they're nullable independently of each other (observed end_epoch set with expires_at: null on 4 of 5 rows in one batch on dev), not as a pair. - status enum only listed "active" with "always active in Phase 1" — real accounts on dev return "expired" too, per the derivation memory-read-api.md already documents. A client generated from the old spec would fail closed on its first real page with an expired row. - Also added updated_at to both NamespaceSummary and MemoryItem, missing from the same schemas for the same reason (spec drifted from the real response shape) — not flagged in review but caught in the same pass. * fix(WALM-295/296/297): close 4 correctness findings from PR #554 review Addresses PR #554 review findings (Henry, GitHub review pullrequestreview-4881462755), all verified against real behavior, not just plausible-sounding: 1. Terminal next_cursor kept the originating snapshot_at, so an incremental-sync client's next poll (using that terminal cursor, never None) stayed frozen at the first walk's snapshot forever — any row updated after that point could never appear again. Fixed by resetting snapshot_at to None specifically on the terminal cursor (has_more: false); continuation cursors mid-walk keep it as before. Applies to both query_owner_namespaces and query_owner_memories. Extended both existing "surfaces on next walk" tests to actually poll with the terminal cursor (the realistic client pattern) instead of only asserting the from-scratch-resync case, which the old buggy code also satisfied trivially. 2. verify_signature stored the caller-supplied x-public-key header string verbatim in AuthInfo.public_key. hex::decode accepts mixed-case input, so the same delegate key could vary casing across requests to obtain independent identities for account-resolution caching and — the actual security-relevant one — read_api_rate_limit_middleware's per-key Redis bucket, trivially defeating that abuse-prevention control. Fixed by re-encoding from the decoded bytes (hex::encode(pk_array)) immediately after decode, so every downstream use sees one canonical form. 3. set_memory_expiry never touched updated_at at all (by design, to stop the ~24h re-verification sweep from making every row reappear in incremental sync) — but that same guard also suppressed the one write that must be cursor-visible: a row's first resolution from unknown to known expiry. A client that had already synced that row before the sweep ran would never see the populated end_epoch/expires_at on any later poll, silently breaking WALM-296 for exactly the rows synced before their expiry was known. Fixed with a conditional bump (IS DISTINCT FROM, null-safe) — advances updated_at only when the value actually changes, satisfying both constraints. Replaced the test that encoded the old (wrong) assumption with one pinning all three cases: first write bumps, unchanged re-verification doesn't, a later real change bumps again. 4. WalrusUploadErrorResponse didn't declare end_epoch even though the sidecar includes it on the metadata-transfer failure response too (not just the success shape) — so it was silently dropped, and recovery via SetMetadataAndTransfer -> FinalizeUploadedBlob finalized with end_epoch = None regardless of what was actually known at upload time. Threaded end_epoch through the full chain: WalrusUploadErrorResponse -> UploadBlobError::MetadataTransferFailed -> WalletOperation's two recovery variants -> the final insert_vector call, mirroring the exact pattern agent_id/package_id already use for the same recovery path. cargo check --lib/--bin: clean. Full suite green against a live database: 354 lib + 494 bin tests (0 failures), including new/updated regression tests for all four findings. Also added the one doc clarification finding 3's fix newly guarantees (memory-read-api.md: expiry sync visibility). * fix(WALM-295): reset snapshot_at on empty continuation pages too PR #554 review (Henry, discussion_r3734942009): the terminal-cursor snapshot reset from the previous fix only fired when the terminal page still had rows. A continuation page (previous page had has_more: true) can come back with zero rows if every remaining row races past snapshot_at between pages -- rows.last() is None either way, so next_cursor collapsed to plain None too. Per this API's contract, an empty page's None cursor means "keep the cursor you already have" -- but the client's held cursor is the previous page's continuation cursor, which still carries the walk's now-stale snapshot_at. That freezes incremental sync the same way the terminal-page bug did, just one page later. Fixed by re-encoding the incoming cursor's own watermark position (no row was consumed, so it hasn't moved) with snapshot_at reset whenever a page comes back empty but there was a real incoming cursor. has_more is always false when rows is empty, so the reset is unconditional there; a brand-new walk that finds nothing at all (no incoming cursor) still correctly returns no cursor. Applies to both query_owner_namespaces and query_owner_memories. Added two new regression tests reproducing the exact race Henry described (a peeked row updated past snapshot_at before the next page is requested), and updated five pre-existing tests that asserted the old "empty page always means None" contract -- each was reached via a real prior cursor, so they now assert a reset cursor instead. cargo check: clean. Full suite green against a live database: 353 lib + 497 bin tests (0 failures). Also merges origin/dev (one real conflict in storage/walrus.rs -- two independent test additions at the same insertion point, both kept). * docs(WALM-295/296/297): clean up internal ticket refs, consolidate migration 011 Henry (internal review): shipped docs and code comments named internal Linear ticket IDs (WALM-295/296/297/298 and unrelated 317/318/319), GitHub PR numbers, and reviewer-thread citations (discussion_r3734942009-style) that read as obscure to devs without tracker/PR access. Stripped all of it from the two public API docs, the OpenAPI spec, .env.example, and every Rust/Python file this PR touches, rewording each comment to state the underlying technical rationale plainly instead of pointing at a ticket for it. Also dropped a dangling reference to an internal design-spec doc that had already been deliberately removed from the repo in an earlier commit. migrations/011_memory_read_api_backfill_updated_at.sql was a permanent no-op: the backfill it originally contained (a single unbatched UPDATE) turned out to crash-loop on a real-sized table under Railway's restart policy, so the real implementation moved into Rust as backfill_updated_at() in db.rs, leaving 011 doing nothing but preserving a file number. Deleted it, folded its explanation into backfill_updated_at()'s doc comment and migrations 010/012's headers, and updated VectorDb::new()'s migration wiring (and the test helper) accordingly. Left the resulting numbering gap at 011 rather than renumbering 012-016 -- documented why in the new migrations/README.md, which also covers the backfill's operational behavior (VectorDb::new() blocks server startup on it, so the first boot after this ships has a startup delay proportional to table size) -- the piece Henry flagged as completely undocumented. While in migrations/012's and db.rs's headers: fixed two unrelated, pre-existing stale references to "migration 014" for the SET NOT NULL step, which is actually migration 016 (014 is the unrelated per-memory-expiry-columns migration). cargo check --lib/--bin: clean. cargo test --lib: 353 passed, 0 failed (comment/doc-only changes in this commit; no behavior touched). * fix(WALM-296): stop expiry re-verification from spamming updated_at daily set_memory_expiry's anti-spam guard compared both end_epoch and expires_at via IS DISTINCT FROM, bumping updated_at only when either changed. But expires_at is recomputed from a fresh now() on every periodic expiry-sweep tick (main.rs's expires_at_from_epoch call), so it differs from the previously stored value on essentially every routine re-verification even when end_epoch hasn't moved at all -- the guard was true almost every time, defeating the exact spam it exists to prevent. Every synced memory would reappear in Console's updated_after incremental sync roughly once a day regardless of whether anything real changed, degrading updated_after into a daily full resync -- precisely the outcome memory-read-api.md promises does not happen. Fixed by comparing end_epoch only; expires_at is still refreshed on every write (it's deliberately approximate, so keeping it current is harmless), it just no longer drives whether updated_at bumps. The existing regression test passed against the buggy code because it re-verified with the exact same expires_at literal, which a real caller never does. Strengthened it to use a different expires_at (same end_epoch) on the "unchanged" case, matching real caller behavior. Live-verified against a throwaway pgvector/pgvector:pg17 container: the strengthened test fails on the pre-fix code and passes on the fix. cargo test --lib against that same container: 353 passed, 0 failed, 0 skipped (every DB-integration test that's normally silently skipped without DATABASE_URL ran for real). Also fixes two next_cursor doc descriptions (memory-read-api.md and the OpenAPI spec, both NamespacesResponse and MemoriesResponse) that had gone stale after an earlier fix (7f0206c) changed empty-continuation pages to return a reset, non-null cursor instead of null. Both now state the real current rule: null only on the first page of a fresh walk that matches nothing; every other empty page returns a non-null, freshly reset cursor superseding whatever the client already held. * fix(migration): delete the 011 no-op placeholder The `SELECT 1;` file was never committed — it was left as an untracked artifact from the pre-commit-guarded attempt. README's reference to it was inaccurate: "no such file" vs "deleted". Correct the README to match reality. Migration numbering is unaffected since 011 was never part of the committed tree. * Fix memory read test migration list * fix(read-api): deletions, metadata backfill, docs CI, token scope 400 Stacked on feat/console-integration-test (PR #554). - WALM-363: namespace_watermarks + memory_tombstones so incremental /namespaces and /memories see hard deletes - WALM-364: metadata_synced_at + refill script backfill-read-api-metadata.sh - Expose importance on MemoryItem; bump snapshot_version to 3 - POST /v1/owner-tokens returns 400 on unknown scope/permissions - Docs: colon routes for freshness, no H1/em dashes, deletion SLA * fix(read-api): atomic deletes, safe 150k backfill, OpenAPI, tests - Wrap tombstone + watermark + DELETE in one transaction - Clear tombstones on live insert; UNION ALL skips ids that are live again - Backfill pages unsynced blob_ids, stamps only that page, never the rest of an owner - Test drives VectorDb::delete_by_blob_id - OpenAPI: importance, status=deleted, snapshot_version 3 - Docs: colon lists instead of semicolon-for-emdash * docs(read-api): restore readable punctuation after em-dash strip * fix(read-api): one sidecar listing per owner, batched SQL for 150k * fix(read-api): WALM-363 deleted[] contract, not status-in-memories Match the Linear-approved design (2026-08-18): - Separate memory_tombstones (memory_id PK); one CTE DELETE RETURNING INSERT - Additive deleted[] + must_resync; memories[] stays live-only - snapshot_version stays 2 so COMG-568 ignores the new keys - Namespace watermark = GREATEST(live MAX(updated_at), tombstone MAX(deleted_at)) - Dual cursor for the tombstone stream; v1 cursor replays 30 days - 30-day retention sweep; security-delete finalization writes tombstones * fix(read-api): must_resync only on stale tombstone cursor, not quiet live rows * fix(read-api): address Harry review on #718 - Identify security-delete purges by memory id (tracking has no namespace; Walrus erase still drops every row pointing at the blob) - Do not pin a tombstone-only first page to snapshot_at (in-flight insert race) - Cursor minted_at so v1 / stale cursors set must_resync - Single TOMBSTONE_RETENTION for sweep and read path - insert_plaintext clears tombstones in the same transaction - GREATEST without redundant COALESCE; docs style (will / e.g. / passive) * fix(read-api): expiry never reaching synced clients, spurious resync, wrong status contract Three defects found reviewing the merged branch. Each ships with a test. 1. expires_at was invisible to a client that had already synced the row. insert_vector writes end_epoch but not expires_at, so when the expiry sweep first resolves a row on the mainline write path, end_epoch is already equal and the `WHEN end_epoch IS DISTINCT FROM $1` guard is false. expires_at went from NULL to a real value with no updated_at bump, so the row never re-entered incremental sync and Console kept expires_at: null for it indefinitely. That is WALM-296 silently not working, on every new memory, for the up-to-5-minute window before the sweep reaches it. The existing test only covered the legacy path where end_epoch was NULL at insert, which is why it passed. Guard now also fires while expires_at IS NULL, which keeps the anti-spam property the comment argues for: a routine re-verification of a populated row still does not bump. 2. must_resync fired on healthy clients. stale_tombstone compared the cursor's deleted_at against the retention cut, but minted_at is re-stamped on every cursor we hand out while deleted_at freezes at the last tombstone the client saw. A client polling every 5 minutes was therefore told to do a full resync 30 days after its last observed deletion, recurring, which on a 150k-row owner is roughly 300 requests. The check was also redundant: the tombstone query already floors on deleted_at >= retention_cut, so a cursor below the floor is subsumed and nothing is skipped or duplicated. must_resync now keys off minted_at alone. Tests pin both directions. 3. The docs described a status value the code cannot emit. memory-read-api.md said status is "deleted" for tombstoned rows. The match on expires_at only ever produces "expired" or "active", and DeletedMemory carries no status field. A Console client following that line would branch on an unreachable value, never evict deleted rows, and leave them visible, which is the exact problem WALM-363 exists to fix. The OpenAPI enum was already correct. Replaced with a pointer to deleted[] as the only deletion signal on this endpoint. Also cleared the last 3 style-guide violations in the two API docs (one Latin abbreviation, two future-tense verbs). --------- Co-authored-by: Harry Phan <phanhoangvinhhien@gmail.com> Co-authored-by: ducnmm <165614309+ducnmm@users.noreply.github.com> Co-authored-by: Harry Phan <lekhacthanhtung.it@gmail.com>
Disable blank issues and require surface, environment, and a repro so placeholder and duplicate-looking reports are harder to file. Point security reports at private advisories instead of the public tracker.
Authorize matched RFC 8252 loopback URIs port-agnostically, then stored the registered port-less URI. The auth code went to port 80 and token exchange rejected the real ephemeral redirect_uri. Store the candidate.
#617) consume_pending_sponsor deleted the Redis record before the sidecar call, so a transport/5xx/429 failure burned the sponsored tx. Restore the binding on those failures so the client can retry. Leave 4xx consumed.
Unregistered 64-hex bearers and missing Authorization no longer open MCP sessions. On-chain lookup failures return 503 instead of 401. Occupancy caps no longer advertise a 30s cooldown. The stdio bridge drains handshake error bodies instead of aborting the socket.
Explicit `login` with no TTY now exits 1 instead of booting the auth-required stub. Concurrent memwal_login calls reuse one listener and URL so a second flow cannot hang later remember/recall.
…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.
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)
HoangDucBach
had a problem deploying
to
benchmark-dev
August 24, 2026 01:38 — with
GitHub Actions
Failure
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
Collaborator
Style Guide AuditAudited 4 file(s) against the Sui Documentation Style Guide. 19 violation(s) found. All must be fixed before merge.
|
fix(mcp): require a registered delegate before opening a session
HoangDucBach
had a problem deploying
to
benchmark-dev
August 24, 2026 02:43 — with
GitHub Actions
Failure
3 tasks
…lyze-gas fix(relayer): restore timeouts, oauth authorize cap, analyze idempotency
HoangDucBach
had a problem deploying
to
benchmark-dev
August 24, 2026 02:56 — with
GitHub Actions
Failure
HoangDucBach
self-requested a review
August 24, 2026 03:04
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.
Merged
This was referenced Aug 25, 2026
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.
Summary
Promote the latest
devchanges tostaging(since #712).Includes:
GET /v1/owners/{owner}/{namespaces,memories,agents}) plus short-lived bearer token auth for Console..memwal/credentials.jsonover the global file); stoplogindeleting credentials before the new ones exist.memwal_logininstead of<no message>.suiprivkeydelegate key, not just hex.stagingis 10 commits ahead ofdev; those are previous promote merge commits only.Test plan
devmemwal_login, remember, recall.memwal/vs global~/.memwal/memwal_*tool shows the login hint