mirror of
https://github.com/block/buzz.git
synced 2026-11-03 07:51:22 +00:00
codex/buzz-269-draft-prep
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
974495fd4a |
fix(mobile): resume reconnect replay from the last live point and spread reconnects (#8659)
## Summary A relay rolling deploy closes every socket with 1012. On reconnect, mobile replayed each live subscription's **original filter** (startup `since`, full `limit`) unless that subscription had received an event, so quiet subscriptions re-read history back to app start every time. Reconnects used a fixed 1s delay with no jitter. The delay also reset to 1s on every successful connect, so a client bounced from pod to pod during a deploy kept coming back at the same moment as every other client. **This PR makes two changes:** 1. **Resume cursor.** If a subscription reached EOSE on a connection, it records `liveUntil`: the time of the last frame received from the relay on that connection, capped at the clock at close. The replay then uses `since = max(original since, lastSeen − 5s, liveUntil − 960s)`, and never goes past the filter's `until`. - **Why 960s:** the relay accepts `created_at` up to ±900s from its clock (`MAX_TIMESTAMP_DRIFT_SECS` in `crates/buzz-relay/src/handlers/ingest.rs`). An event stored after the close can therefore be backdated by that much, and the extra 60s covers client clock skew. - **Only relay frames count as liveness.** Close and error paths never extend `liveUntil`, so a close noticed after the device sleeps doesn't overstate how long the subscription was live. - **Ambiguous EOSE is ignored.** If a subscription has a second REQ on the same socket, that subscription doesn't advance `liveUntil` for the rest of the socket. A CLOSED ends liveness for the rest of that socket, because the retry is a second REQ. Subscription IDs are never reused, so an old EOSE can't mark a re-subscribe as live. - **No cursor:** a subscription that never reached EOSE keeps its previous cursor, or replays as before. 2. **Reconnect spreading.** - After a 1012 or 1001 close, the first attempt waits a uniform random delay in [0, 10s]. A client already backing off waits the longer of that delay and its backoff. - Ordinary backoff gets symmetric ±25% jitter, capped at 30s. - The backoff resets only after a connection stays up for 60s, not on connect. This matches desktop. - Foreground resume and a manual `reconnect()` still reset to the base delay, as before. **Simpler?** The pure reconnect math (cursor, jitter, spread, stability) lives in a new `relay_reconnect_policy.dart` and is table-tested on its own. `relay_session.dart` was near its 1200-line cap, so its five small private bookkeeping classes moved verbatim to `relay_session_subscriptions.dart`. Net concept count is unchanged. **Identified debt (not changed here):** - Dart's WebSocket ping/pong isn't visible to the session, so a long-idle but healthy connection that drops replays back to its last application frame − 960s. This is still bounded, and much cheaper than replaying from app start. - The unread catch-up in `channels_provider.dart` runs one REQ per active channel (`since = readAt + 1`, `limit: 1000`) on reconnect and every 60s. - A retryable CLOSED is retried with a 30s cap, no jitter and no attempt limit. - `lastSeenCreatedAt` advances on stored events received before EOSE. If a relay returns stored events newest-first and the socket dies before EOSE, the next replay can skip older stored events. - The `lastSeen − 5s` term keeps its existing gap for events backdated after a close. ### Related issue N/A. This is part of a set of reconnect-load reductions. Companion PRs: relay read-pool warm (#8651), desktop (#8652), agent harness (#8653). ### Testing - Mobile gate on a Linux workstation, at the head SHA: - `dart format --set-exit-if-changed`: pass. - `flutter analyze`: no issues. - Gateway recipe tests: pass. - `flutter test`: 3881 pass, 1 fail. `push_gateway_origin_validator_test` fails the same way on `main` on that host, due to a host git-config error. - New `relay_reconnect_resume_test.dart`: - pure table tests for the cursor and the delay - a real-socket test that reads the 1012 close code - journeys through `RelaySessionNotifier`. They check that a close or error after an hour asleep doesn't extend liveness, that a second REQ or a delayed EOSE for an unsubscribed ID can't advance the cursor, and that a backdated event inside the 960s window is still replayed. - 22 production mutations, each failing the new tests. They cover every cursor term, the 960s window, the `until` clamp, the liveness cap, the ambiguous-EOSE guard, CLOSED handling, jitter, the 1012 spread and floor, the backoff reset, and close-code capture. - `fake_relay.dart` now draws a fixed jitter midpoint, so existing journey tests stay deterministic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
4ff02bf03b |
perf(mobile): sign events with native libsecp256k1 on iOS and Android (#8654)
Mobile signs every Nostr event (sends, relay AUTH, media auth headers, preference writes) in pure Dart on the UI isolate. On an iPhone SE (2nd gen, Profile build) one BIP-340 signature costs about 10.7 ms and deriving the public key another 5.6 ms, so a burst of signed writes drops frames. This adds `mobile/packages/buzz_secp256k1`, a small FFI package that builds vendored bitcoin-core/libsecp256k1 v0.8.0 (ecdh, extrakeys, schnorrsig modules) through a Dart build hook, so the same C and the same Dart API ship on iOS and Android with no platform bridge code. All 13 `nostr.Event.from` signing sites now go through `signNostrEvent`, which keeps the `nostr` package's event-id serialization and moves only key derivation and signing to native code. Calls stay synchronous. The package also exposes `schnorrVerify` and `ecdhX`; follow-up PRs move verification and the NIP-44 conversation key over. Incoming relay events are still not verified, unchanged by this PR. Safety: the signing context is randomized once per isolate, secret keys and keypairs are wiped from the native scratch buffer after each call, aux randomness comes from `Random.secure()`, and the static verify context runs libsecp256k1's self test before first use. ### Validation - Added tests: BIP-340 official vectors and native-vs-pure-Dart differential tests in the package; `event_signing_test.dart` signs with 64 random keys and checks pubkey, event id and signature against the existing `nostr` builder and verifier, plus malformed-key rejection. - Full mobile analyze and test suite pass locally. - Builds: iOS release (framework ~1.3 MB) and Android debug APK with the library for arm64-v8a, armeabi-v7a and x86_64 (~1.2–1.3 MB each). - On-device (iPhone SE 2nd gen, Profile, median per op): sign 10,736 µs → 41 µs, derive pubkey 5,580 µs → 16.5 µs, full signed event 10,744 µs → 65 µs. Event-id JSON and SHA-256 are 23 µs, so they stay in Dart. - Device, iOS (iPhone SE 2nd gen, Profile): `buzz_secp256k1.framework` embedded; the signed-in app authenticated to a real community relay, loaded channels and history, and paged older messages. No native-asset or secp256k1 errors in the console. - Device, Android (emulator, debug): APK contains `libbuzz_secp256k1.so` and loads it. Against a local mock relay that verifies with the pure-Dart `nostr` package, the app's NIP-42 AUTH, NIP-98 `POST /query` headers, and kind 20001/30078 events all verified. - Blossom GET auth, iOS: the dev relay returns 401 for unauthenticated `/media/` GETs. From inside the running app, the production `MediaGetAuthService.headersFor()` header for a nonexistent hash got 404 (accepted, file missing), compared with 401 without it. The community has no relay-hosted image, so none was rendered. buzz-review-completed (agent review, Codex and security review clean; device validation above; human app test not applicable under the mobile testing exception) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b6aa5539e3 |
feat(mobile): account onboarding, key recovery and hosted reconnect (#8375)
## Behavior Adds optional mobile Builderlab sign-in, identity backup/recovery, community discovery/creation, and account-aware reconnect. **Builderlab features are disabled by default:** when `BUZZ_AUTH_DOMAINS` is unset or empty, account login, creation, backup/recovery, reconnect prompts, and background account restoration are hidden/disabled. QR/code pairing, links, and invites retain Nostr-only entry. Saved account state is retained for later opt-in. `--dart-define=BUZZ_AUTH_DOMAINS=communities.buzz.xyz,communities.public.builderlabstaging.xyz` opts those DNS domains and their subdomains into required account setup. The same policy gates pairing, deep links, durable community admission, and reconnect. A nonempty list enables Builderlab features; `BUZZ_BUILDERLAB_URL` alone does not. Enabled flows still require their backend services. Existing Nostr relay authentication remains mandatory. `BUZZ_HOSTED_RELAY_AUTH=true` enables scoped relay assertions and reconnect prompts only for configured authentication domains. Configure assertion issuance and each community's issuer binding before enabling assertions; enable relay enforcement last. Account credentials stay at Builderlab. Wrong-account recovery preserves keys; failed writes are not replayed; authentication is bounded by request deadlines. Hosted flows preserve keys across cancellation/restart, reconcile interrupted creation/invitation acceptance, and support manual nsec file import/export. Account identities are scoped to backend and authenticated subject; reusing an existing local key for an unbound account requires an explicit selection. Recovery reconciles the advertised and recovered inventories before automatic selection. Successful self-hosted admission retires its pending key, and iOS startup removes interrupted export files. Adds authenticated relay `GET /operator/communities/joined` for the companion Builderlab membership endpoint. ## Validation - Follow-up review repair at `66a12ac92`: self-hosted entry presents readable choices and import despite incomplete enumeration, while generation still requires a complete empty inventory; explicit selection uses exact-key lookup. Consuming a value-equal duplicate now advances dispatch. Reconnect requires an existing account binding or a subject-scoped backup receipt before signing an assertion, and pins the subject inside the serialized account request. An empty different account is refused without sending key proof. Focused checks: configured domains 58 passed; default 49 passed, 9 hosted-only skips. Separate isolated mutations fail the incomplete-inventory sheet, exact-key reuse, duplicate dispatch and reconnect journeys (including an empty-account-only bypass); restored controls pass. Evidence uses synthetic storage/network boundaries, not physical Android or live backend association. All mandatory commit/push hooks passed; fresh GitHub CI is pending. - Bounded review repair at `d3cc0003f`: incomplete secure-storage enumeration now blocks automatic identity generation while explicit known-key reads retain their fallback; declined/failed onboarding links release the queue; HTTP 429/5xx keeps Retry regardless of response encoding. Default app-gate coverage exercises a bind link followed by ordinary community entry. Focused checks: configured domains 47 passed; default 38 passed, 9 hosted-only skips. Removing the three production fixes in an isolated source/Git/build copy fails the identity-resolution, declined/failed queue, and Retry journey assertions; clean and restored controls pass. Storage and HTTP failures are synthetic; physical Android and live outage behavior were not exercised. Published after all mandatory commit/push hooks passed (mobile analysis/tests, branch-skew and file-size gates); fresh GitHub CI remains pending. - Existing whole-PR source review cleared the reconnect defects at `fb15d5b82`. - Domain-policy increment: focused environment, entry, app-gate, pairing and onboarding journeys pass with empty and configured domains. Existing pairing provider/page tests pass. Changed Dart files analyze cleanly. - Isolated source/Git/build mutation: forcing paired credentials through hosted login fails the default-build journey; clean and restored controls pass. CI now runs configured-domain journeys in addition to the default-build suite. - Integrated main through `5b433d929` at `0067c069c`, including its pinned Flutter upgrade. Preserved the PR welcome UI with the updated transition API and added Material surfaces around onboarding selection/consent rows. Focused post-merge runs pass in both configurations; all mandatory local push gates passed (mobile analysis/tests, Rust tests, desktop-native checks, branch-skew and file-size checks). - Merge-review repair at `a315bce11`: focused identity/storage/admission and widget journeys pass, including account switching with an empty or already-bound second account, explicit reuse, and self-hosted removal/rejoin with another key. Default-domain journeys pass too (hosted-only cases intentionally skip there). - Isolated source/Git/build mutation checks: bypassing account scoping, reuse consent, pending-key retirement, or recovery inventory reconciliation fails the relevant journey/invariant assertions; clean and restored controls pass. - Native `RunnerTests/IdentityFilePluginTests`: both tests pass on the iPhone 18 Pro iOS 27 simulator with an isolated app identifier. At `24a0dcaac`, the picker journey seeds residue in Library/Caches, matching path_provider_foundation, and verifies plugin startup removes it while preserving an unrelated file. The earlier tmp-only fixture did not exercise the actual export location. This is synthetic native cleanup/picker evidence, not a real terminated-share or live account round trip. - Follow-up: Caches and tmp cleanup; fallback to individually known identity slots when Android bulk enumeration fails; explicit replacement of a refused self-hosted key preserves its original before swapping the retry slot. Configured-domain journeys/invariants: 34 passed; default: 31 passed, 3 hosted-only skips. Isolated enumeration/retry mutations fail with passing controls. Bulk-read fallback preserves but cannot enumerate arbitrary other-account slots until bulk reads recover. - Configured-domain CI exposed a stale pairing-status assertion after account scoping. The repaired test checks the admission mismatch, preserved account/transfer keys, and no admitted community. The configured-domain CI selection passes locally (56 tests); removing the production mismatch guard fails the updated test, with clean/restored controls passing. This is synthetic coordinator evidence, not physical QR acceptance. - Final review repair (`40e1e6101`): native iOS export creation uses atomic complete file protection; normal and restart cleanup remain. Account/policy/claim HTTP uses one 30-second total deadline with request abort and body cancellation. Removed the unrelated NIP-FA spec edit. - Focused account/admission/export tests: 25 pass; configured-domain account/admission/onboarding journeys: 33 pass. Isolated deadline and native-dispatch mutations fail, with passing clean/restored controls. Native export/picker tests: 2 pass on iOS 27 simulator. The real file-protection attribute assertion is physical-device-only because simulator provides no Data Protection attribute; locked-device protection and interrupted real sharing have not been observed. - Opt-in UI: configured-domain CI selection passes (57 tests); default selection passes (7 tests, 10 hosted-only skips); URL-only plus whitespace/empty domains passes (6 tests, 9 hosted-only skips). Changed Dart files analyze cleanly. Independent mutations that unconditionally initialize the coordinator in the app gate, welcome entry, or link form fail the default journey; clean/restored controls pass. - Native welcome screens exercised on iPhone 18 Pro / iOS 27 simulator in default and configured builds: default exposes community link / QR / pairing code only; configured build additionally exposes Builderlab connection and community creation. No live hosted login was exercised in this check. - Delivery at `e48fe73a9`: all mandatory push hooks passed (full mobile analysis/tests, Rust tests, desktop-native checks, branch and file-size checks). GitHub checks had not yet populated at the immediate post-push snapshot. ## Review disposition | Feedback | Disposition | | --- | --- | | Explicit self-hosted entry under unreadable inventory, duplicate invite dispatch, reconnect cross-account binding ([entry](https://github.com/block/buzz/pull/8375#discussion_r4318421622), [queue](https://github.com/block/buzz/pull/8375#discussion_r4318421640), [reconnect](https://github.com/block/buzz/pull/8375#discussion_r4318455512)) | Repaired at `66a12ac92`; evidence above. Reconnect refuses unproven ownership rather than silently establishing a binding. | | Incomplete Android identity inventory, stranded deep-link queue, transient HTTP retry classification ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5576864143)) | All three repaired in `d3cc0003f`; focused journeys and isolated mutation evidence above. | | Account cross-link, incomplete recovery inventory, committed invite checkpoint, pending local key retirement/replacement | Repaired in the preceding commits; journey and mutation evidence above. | | iOS interrupted key export ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5575253252)) | Native cleanup plus complete protection at creation implemented. Physical-device lock and real interrupted sharing acceptance remains pending. | | Trickle responses bypass idle timeout; unrelated NIP-FA edit ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5575764556)) | Total request deadline/abort implemented; spec edit removed. | | Production domains rejected | Ordinary entry now needs no Builderlab configuration. Opt-in account-required domains must still use supported community mappings. | | Lost Android Keystore recovery | Backup exclusions prevent the reviewed stale-restore scenario; genuine key loss still has no in-app recovery/reset and remains deferred. | | Native secp256k1 dependency question | Obsolete against the current base: upstream C integration landed in #8398; no package diff remains here. | | Operator membership endpoint contract | Documentation now states the operator chooses the pubkey; no end-user session binding is implied. Additional handler-level coverage is optional follow-up. | | Optional Android partial document cleanup/activity retention; local work after backup cancellation | Follow-up hardening, not claimed implemented. Cancellation prevents remote backup upload. | | Semgrep crypto inventory notices | Informational inventory, not demonstrated defects. | ## Remaining draft gates Native QR/browser-return acceptance remains unverified for this revision. Optional Builderlab onboarding still needs backend backup-status and joined-community routes, recovery policy/configuration, and real backup/recovery/reconnect acceptance. The empty-domain build removes these services as prerequisites for ordinary pairing; it does not make the optional hosted flows backend-ready. Human acceptance and current CI are still required before merge. Review dispositions retained: production auth-domain configuration must use supported community mappings; enabling `builderlab.xyz` does not add support for `buzz.block.builderlab.xyz`. Recovery after genuinely losing an Android Keystore key remains a follow-up, not a newly implemented capability. Human acceptance steps: build once without `BUZZ_AUTH_DOMAINS` (including with a previously saved account) and confirm no Builderlab controls/restoration while ordinary link and QR/code entry work. Build with supported domains and confirm connection/creation appear; exercise browser return and live backup/recovery/reconnect. On physical iOS, exercise export and interrupted sharing/locked-device protection. Record results before marking review complete. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@wes.communities.public.builderlabstaging.xyz> |
||
|
|
ce56e34411 |
fix(mobile): recover stale relay sessions (#4372)
### Summary Fixes [this issue](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=b726c366abfe62429ee3cdcd34d0c0fb98c33c3ea053480585bed71745412b56): > I often don’t see my bot responses until after I post. they’re usually time stamped correctly so I think it’s just a refresh issue? ### What changed? Buzz Mobile now reconnects relay sessions after the app has remained backgrounded beyond the existing 5-second grace period, even when the session still reports a stale `connected` state. This makes resume recovery independent of whether iOS runs the grace timer before or after delivering `resumed`. Reconnection is now based on elapsed background time rather than a direct socket-health probe. - If the app was backgrounded for at least the 5-second grace period, the socket is presumed dead and the session reconnects regardless of reported status. - If it was backgrounded for less than that, a reported `connected` status is still trusted. In the sub-5-second window the socket is either genuinely alive, which is the common case for a momentary background, or it is dead and the client ping detects it within the two-interval worst case described below. That is now a degraded-latency path, not a silent-forever path. The mobile relay socket now uses `IOWebSocketChannel.connect` with a 30-second `pingInterval`. An unanswered ping closes the Dart socket through the existing disconnect and reconnect path. Detection takes up to two ping intervals, so about 60 seconds worst case, not 30. One interval of idleness elapses and a ping is sent, then a second interval elapses with no pong and the socket closes. Any inbound pong restarts the first stage, so the clock measures idleness rather than running on a fixed cadence. ### Why? Buzz iOS can sometimes stop showing new bot or agent responses after a phone has been locked for 5 to 10 minutes. When the user later posts a message, the missing responses can appear all at once. iOS may suspend Buzz before the short delayed cleanup that would normally close its connection has a chance to run. Before this change, Buzz trusted the resulting stale healthy status on resume and skipped reconnecting, so the missing responses stayed hidden until a later post exposed the dead connection. A state-machine test with a stubbed connection reproduced this reported pattern and showed that it matches this failure mode: the failed post triggered a reconnect that fetched the missing messages. The same test also checked the other candidate explanation, the bug tracked in [#3053](https://github.com/block/buzz/pull/3053), where the relay has closed the app's subscription. That state does not produce the pattern. Posting succeeds and the user's own message appears, but nothing looks for the missed messages, so they stay hidden. The test confirmed that the missed messages were still available to fetch in that state, so the missing step was a trigger to fetch them. This was not an end-to-end reproduction on an iOS device or a live relay. The new resume check covers the normal lock and unlock path. If the app was backgrounded for less than the 5-second grace period, it still trusts a connection marked as healthy. A dead connection in that window is instead detected by the ping check, which can take up to about 60 seconds but prevents the app from remaining silently stuck. The ping only runs while iOS is running the app, so it does not detect a connection that died during suspension; the resume check owns the lock and unlock path. A pre-existing path also runs the same resume handling when network connectivity returns while the app is already in the foreground. Because the app was not backgrounded, this change does not alter that path, which still trusts a connection marked as healthy and relies on the slower ping check. Recovery from a subscription that the relay explicitly closes remains in [#3053](https://github.com/block/buzz/pull/3053), and the two changes overlap in one file. Changes to how missed messages are backfilled or replayed are out of scope. ### How is it tested? Full mobile suite at base and head. Both runs have the same known macOS-host-only failure in `ChannelDetailPage keeps follow mode off while a tall newest message stays visible` at line 1053: - Base: 1,021 passed, 1 skipped, 1 failed - Head: 1,025 passed, 1 skipped, 1 failed Added tests: - [`relay_session_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_session_test.dart): long-background resume reconnect and within-grace control - [`relay_socket_liveness_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_socket_liveness_test.dart): silent-peer disconnect and idle-but-healthy control Mutation checks confirm that removing elapsed-background resume recovery fails with one socket instead of two, and removing `pingInterval` leaves the silent peer connected. Restored production code passes both mutations' regression tests and the healthy idle control. Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> |
||
|
|
3112e59fd8 |
fix(mobile): add mentioned agents to channels (#1696)
Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> |
||
|
|
dbe7fb8534 |
Fix mobile relay reconnect lifecycle (#1772)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> |
||
|
|
fbbb2d1d6e |
fix(deps): update dependency pointycastle to v4 (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
454781caae | Sprout speaks Nostr. Nothing else. (#475) | ||
|
|
57186963e3 |
feat(mobile): multi-workspace support (#408)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
94d4e16461 |
feat(mobile): websocket streaming with real-time messaging (#323)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |