test: delete#1122
Closed
janthurau wants to merge 33 commits into
Closed
Conversation
RFC 0001 lays out the architecture for rewriting the hocuspocus server in Rust on top of yrs: per-document actor concurrency, a trait-based extension system mirroring the 25 TS hooks, byte-compatible wire protocol and Redis interop, a webhook contract for auth/persistence, and a conformance strategy that reuses the existing TS test suite as the product spec. PORTING.md pins the explicit TS-to-Rust construct mapping, the hook semantics table, and the byte-exact / semantic / allowed-to-differ classification that gates every milestone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Eight crates per RFC 0001: - hocuspocus-protocol: lib0-compatible varint/varstring codec, message envelope with session-aware routing keys, auth submessages incl. the optional trailing providerVersion, bare ping/pong frames, close-code constants, and the Redis pub/sub frame codec - all with unit tests against hand-derived byte layouts from the TS implementation - hocuspocus-core: the Extension trait (all 25 hooks, default no-ops), HookChain with priority ordering and SkipFurtherHooks/Abort semantics, Context (JSON + typed halves), Storage and Authenticator traits, Origin, and the document actor type skeletons (DocMessage/DocHandle/DocRegistry) - hocuspocus-webhook: HMAC-SHA256 request signing byte-compatible with @hocuspocus/extension-webhook - hocuspocus-storage: Storage trait impl surface with InMemoryStorage - hocuspocus-axum, hocuspocus-redis, hocuspocus-metrics: config and constant surfaces for M2-M5 - hocuspocus-server: binary that loads TOML+env config (defaults mirror the TS defaultConfiguration), binds, prints a machine-readable ready line for the conformance harness, serves / and /healthz, and shuts down gracefully on SIGTERM/SIGINT cargo build/test/clippy -D warnings/fmt all green; 31 unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Path-gated to crates/** and the Cargo manifests so JS-only PRs are unaffected; reuses the Redis service-container pattern from build.yml for the multi-node interop tests landing in M4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
A Node script that drives the real TypeScript implementations (OutgoingMessage, @hocuspocus/common auth encoding, lib0, y-protocols, yjs) to produce the committed fixture corpus consumed by the Rust protocol crate's conformance tests: every message type, session-aware addresses, auth frames with and without the trailing providerVersion, real yjs sync steps and state vectors, awareness updates including null-state removals, and the Redis pub/sub frame layout. Adds y-protocols as a tests-workspace dev dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
… (M1) - SyncMessage codec: y-protocols sync subtypes (Step1/Step2/Update) over opaque yjs update bytes - AwarenessUpdate codec: structured clientId/clock/state-JSON entries, enabling before_handle_awareness hooks to mutate states without the scratch Y.Doc the Node server allocates per frame; allocation bounded against absurd entry counts - Golden tests over the TS-generated fixture corpus: all 24 fixtures decode, semantic content matches, envelopes/awareness/redis frames re-encode byte-exact, and - the key compatibility check - a full update produced by real yjs applies cleanly to a yrs document, yields identical content, and round-trips through yrs re-encoding Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Three targets covering attacker-controlled inputs: the full inbound frame path (envelope + type-specific payload decoders), the Redis pub/sub frame decoder, and the awareness update decoder with re-encode. Corpus seeded from the golden-vector fixtures. Smoke runs: ~22M total executions across the targets with zero crashes. Run with: cargo +nightly fuzz run <target> from crates/hocuspocus-protocol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Real @hocuspocus/provider clients now sync against the Rust server: - hocuspocus-core engine: per-document actor tasks exclusively owning the yrs::Doc, replicating MessageReceiver.ts semantics - SyncStep1 gets Step2 + the server's own Step1, applied updates broadcast to all subscribers and ack with SyncStatus, SyncReply suppresses the reciprocal first sync step (server-internal, never sent to clients) - server-side awareness store wire-compatible with y-protocols clock rules, per-connection client-id tracking, and removal broadcast on disconnect - without the per-frame scratch Y.Doc the Node server allocates - per-socket connection task: pre-auth queueing with the exact TS limits (5 MiB / 1000 messages / 100 pending documents), the non-refreshable pre-auth deadline, post-auth idle timeout (4408), PermissionDenied + 4401 on auth failure, per-document multiplexing over raw routing keys - debounced persistence (debounce/maxDebounce semantics) through the Storage trait; documents unload when empty and reload from storage - hocuspocus-axum: WebSocket transport pumping engine channels - hocuspocus-server: WS upgrade on any path, control API (/control/stats, /control/close-connections), graceful shutdown that closes clients with 4205 and flushes pending stores Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…erver Covers the auth handshake, initial sync, bidirectional propagation, SyncStatus acks (hasUnsyncedChanges), awareness propagation and disconnect cleanup, late-joiner full sync, document isolation, the control API, and unload/reload through storage. All checks pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- HOCUSPOCUS_TEST_TARGET=rust switches newHocuspocus() to spawn the Rust server binary; the shim exposes the surface Tier-1 tests use (webSocketURL, closeConnections via the control API) and fails fast with a clear message for in-process hook options until the M3 webhook transport lands - tests/conformance/rust-target.json is the skip-map: 6 applicable files today (21 tests, all passing), everything else annotated with the milestone that unlocks it - pnpm test:rust prints the conformance dashboard; a new CI job builds the binary and runs it on every Rust or conformance change - the server binary no longer rejects unrelated HOCUSPOCUS_* env vars (HOCUSPOCUS_TEST_TARGET leaked into the figment namespace) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- WebhookAuthenticator POSTs signed auth events ({event, payload} with
X-Hocuspocus-Signature-256) and maps 200 {context, scope} / 403
{reason} to the auth decision
- server binary: [auth] mode = "none"|"webhook" + [webhook] url/secret
config (env-mappable for the test harness)
- auth failure now matches TS exactly: PermissionDenied is sent and the
socket STAYS OPEN with that document's state reset, so provider token
retries work on the same connection (previously closed with 4401)
- test shim serves onAuthenticate closures through a per-test HTTP
webhook receiver - the production contract doubles as the test
transport; onAuthenticated/onAuthenticationFailed/retry files moved
to the conformance run list
Dashboard: 9 files / 28 tests passing against the Rust binary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
onConnect, onDisconnect, onClose, onAwarenessChange, onAwarenessUpdate and onMessage use provider-side callbacks only (no server hooks) and pass against the Rust binary unmodified. Re-annotate the remaining skips with the precise blocking feature (webhook load/store, server onStateless hook). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…read-only acks
- WebhookStorage: GET/PUT {base}/documents/{name} binary endpoints
(signed), selected via [storage] backend = "webhook"
- EventHooks trait: connect (awaited before auth, may reject) and
disconnect notifications; WebhookEvents delivers them per the
[webhook] events config
- read-only Step2 acks now match TS snapshotContainsUpdate behavior:
saved=true when the document's state vector covers the update
- config env nesting switched to double underscore
(HOCUSPOCUS_SERVER__LISTEN) so multi-word fields are addressable;
scalar test options (debounce, timeout, unloadImmediately) map to env
- test shim serves onLoadDocument/onStoreDocument/onConnect/onDisconnect
closures through the per-test webhook receiver
Dashboard: 17 files / 61 tests passing against the Rust binary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- stateless webhook event: the actor delivers client Stateless payloads to the EventHooks trait (awaited inline, preserving the per-document ordering of the TS message queue); WebhookEvents posts them per the [webhook] events config; test shim serves onStateless closures - SqliteStorage: byte-compatible with @hocuspocus/extension-sqlite (identical documents(name, data) schema and upsert), so Node and Rust fleets can share one database file during migration - PostgresStorage: same storage contract over bytea rows - both behind cargo features (sqlx, rustls); [storage] backend = memory|webhook|sqlite|postgres in the binary config Dashboard: 18 files / 62 tests - the ENTIRE tests/provider/ and tests/providerwebsocket/ suites now pass against the Rust binary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- Scaler trait in core: attach/detach per loaded document plus try-once store-lock coordination (lost lock = another instance persists, matching the SkipFurtherHooksError semantics) - document actors gain a pub/sub relay: on attach they publish their first sync step and query peer awareness (the Node extension's afterLoadDocument bootstrap); peer Step1 gets Step2 + own Step1 as SyncReply; non-redis-origin changes publish the server's Step1 (Node's onChange); awareness changes relay while local connections exist; redis-origin changes never schedule stores - hocuspocus-redis: redis-rs RESP3 push connection with per-channel routing into actors, identifier self-echo filtering, per-document publish forwarders, SET NX PX + compare-and-delete Lua store lock on the same keys as the Node Redlock usage - [redis] url/prefix/identifier config on the binary Verified by a three-topology smoke suite against a real Redis: two Rust instances, and - the migration path - a REAL Node @hocuspocus/server with extension-redis sharing Redis with the Rust binary in both directions. Edits, awareness, and late-joiner full state all propagate cross-instance in all topologies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- the shim maps Redis extension instances from test options onto the binary's [redis] env config, so the multi-server extension-redis AVA tests now run against Rust-Rust topologies unmodified - server-side broadcast_stateless: engine API + control endpoint (/control/broadcast-stateless), relayed to peer instances unless the message itself arrived via the relay (no ping-pong) - stateless webhook responses can carry a respond payload, delivered back to the originating connection - the webhook equivalent of the onStateless hook's connection.sendStateless() - CI conformance job gains the Redis service container Dashboard: 21 files / 68 tests passing (extension-redis onChange, onAwarenessChange, onStateless included; onStoreDocument stays skipped - two of its tests inject custom in-process TS extension classes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The tests/server/unauthenticatedQueueLimit suite exposed three real gaps, all fixed: - authentication now runs on a spawned task per document (TS behavior: a slow or never-resolving onAuthenticate must not stall the frame loop), with an Authenticating state that queues frames under the same byte/message limits and counts toward maxPendingDocuments - including in-flight auths - pre-auth limit violations close with ResetConnection 4205 (was 1009), matching the TS server - the axum transport drains the socket (bounded, 3s) after an engine-initiated close while the client is mid-flood: dropping a socket with unread inbound data sends TCP RST, destroying the in-flight close frame (clients saw 1006 instead of the close code) - maxUnauthenticatedQueueSize/Messages and maxPendingDocuments are now configurable on the binary and mapped from test options Conformance run list is now strictly exit-code-verified: 23 files / 76 tests. tests/server files mixing wire tests with in-process-API tests are annotated in the skip map pending per-test granularity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- rust-target.json gains a 'partial' section: files mixing wire tests with in-process-API tests run in their own ava invocation with the in-process tests excluded via negative --match patterns - getConnectionsCount()/getDocumentsCount() are now shimmed as sync getters backed by a 50ms control-API stats poller (tests read them inside retryable assertions, so eventual consistency suffices) - stats 'connections' now reports the TS semantic - established document connections, counted by the actors - instead of open sockets (a socket with failed auth counts zero, which the auth-failure tests assert); sockets are reported separately - shim passes providerVersion through to onAuthenticate payloads Dashboard: 28 files / 97 tests passing (76 full + 21 partial), strictly exit-code verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…n load failure Two behavioral differences surfaced by tests/server/onStoreDocument and onLoadDocument: - a failed store no longer unloads the document: the actor stays loaded with the state re-marked dirty so the debounce timer retries, and unload only happens once the final store succeeds (TS keeps documents in memory when onStoreDocument throws - no data loss) - a failed document load sends PermissionDenied and resets the per-document state with the socket kept open (TS setUpNewConnection catch path), instead of closing with 4403 Full dashboard (28 files / 97 tests) verified unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- both Redis connections use ConnectionManager: exponential-backoff reconnects, and the pub/sub connection re-subscribes all document channels automatically after a reconnect - a silently dropped subscription would partition a document across the fleet - the per-document publish forwarder retries briefly (5 x 500ms) when a publish races a reconnect, instead of silently dropping the frame - topology smoke gains a restart-resilience section: Redis is SIGKILLed and restarted mid-session; edits made after the restart must still propagate between instances (verified passing), with bounded teardown so a wedged Node extension can't mask results Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
ObjectStoreStorage works over any object_store backend (S3, GCS,
Azure, MinIO, filesystem, in-memory) with the exact key layout of
@hocuspocus/extension-s3 ({prefix}{documentName}.bin, full yjs update
v1 per object), so Node and Rust fleets can share one bucket during
migration. Verified by an in-memory roundtrip test asserting the
Node-compatible object key.
Binary config: [storage] backend = "s3" with bucket/region/endpoint/
prefix; MinIO-style endpoints get path-style addressing and http is
allowed for local endpoints. Credentials come from the standard AWS
environment variables.
Full conformance dashboard verified green after the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The actor fires a change event on the debounced store cadence carrying the incremental yjs update (base64) since the previous event - apps apply it to their own Y.Doc copy (the documented transformer-sidecar pattern replaces the in-process Tiptap transformer). Redis-origin changes never fire on the receiving instance. Enabled via [webhook] events = "change"; verified by a committed smoke that decodes the payload and asserts the document content. This completes the TS webhook extension's event surface (auth/connect/disconnect/change/stateless) on the Rust binary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Measures the failure mode that motivated the rewrite: edit propagation on a small document while loader processes hammer an 8M-char document with fresh initial syncs (each a full-state encode server-side). Results (16-core container, release build): Node's probe latency degrades 16x at p50 (2ms -> 33ms; p99 116ms) because the single thread couples every document to the busiest one. The Rust server's p50 is UNCHANGED under load (1ms; p99 12ms) while serving 76% more initial syncs (648 vs 368 in 12s). Recorded in the RFC. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Text exposition of the engine gauges: established document connections (TS getConnectionsCount semantic), open sockets, and loaded documents. Message/store counters follow with the metrics-facade instrumentation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The connection context established at auth time now flows everywhere the
TS server delivers it:
- Storage::fetch/store carry the triggering connection's context;
WebhookStorage sends it as an X-Hocuspocus-Context JSON header on the
GET/PUT persistence requests (TS onLoadDocument/onStoreDocument
`context` payload field).
- EventHooks::connect may now contribute context: the connect webhook's
`{"context": {...}}` response merges into the connection context under
the auth response's keys (TS order: onConnect first, onAuthenticate on
top). disconnect/change/stateless event payloads carry the context of
the connection that caused them.
- The document actor tracks the last change's context for store/change
events; DocMessage::Join and the socket's Active state carry the
connection context.
Conformance shim: the per-test webhook receiver now runs unconditionally
(auth, storage, and connect/disconnect/stateless events always routed
through it), which also makes `server.configure({...})` after
construction work — hook closures merge into the live dispatch. Two
previously excluded context tests now run and pass:
- onAuthenticate.ts "passes context from onAuthenticate to onLoadDocument"
- onDisconnect.ts "passes the context to the onLoadDocument callback"
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
When a test fails inside a bare timer (the hasUnsyncedChanges pattern), the ava worker dies from the uncaught exception WITHOUT running teardowns; any ref'd handle the shim holds then keeps the worker's event loop alive and the whole batch hangs after its summary instead of reporting the failure. Unref everything the shim owns: the webhook receiver and its sockets, the spawned binary's process watch, and its stdout pipe (nothing is read after the ready line). Also scope the auth and persistence webhook wiring back to spawn-time hooks: the extra webhook hop on every connection/document load added enough latency under full-batch load to flake timing-sensitive tests. Lifecycle events (connect/disconnect/stateless) stay always-on — that is all `configure()`-added hooks need. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The always-on connect event added a second sequential webhook round trip through the (batch-saturated) test process's event loop before every authentication, which deterministically blew the suite's tightest fixed deadline — hasUnsyncedChanges' 200 ms readonly test — in two consecutive full-dashboard runs. Enable connect/disconnect/stateless events only when the test passed lifecycle hooks up front, or when the server was created bare (the configure()-after-construction pattern that relies on them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…tics Server fixes surfaced by triaging the two remaining tests/server files into the conformance run: - unloadImmediately=false now defers the final store to its natural debounce deadline after the last disconnect (TS lets the debouncer run; only unloadImmediately=true flushes at once). - Failed document loads propagate an explicit denial reason: a new typed `Refused` error carries the webhook 403 body's `reason` into the client's PermissionDenied message (TS `error.reason ?? "permission-denied"`); other errors stay generic. Conformance shim robustness (root cause of the batch hangs found): - The spawned binary no longer inherits the ava worker's stderr pipe — an orphaned server (worker crashed before teardowns) held that pipe open and ava's main process waited on its EOF forever. Server logs are opt-in via HOCUSPOCUS_RUST_STDERR=inherit. - The stats poller uses plain node:http with agent:false instead of fetch(), whose global undici pool keeps ref'd keep-alive sockets alive. - onLoadDocument return values are only adopted when they are a Y.Doc (TS ignores anything else, e.g. `return true`). - Denial responses carry `error.reason`, not Error#message. - server.documents gains a poller-backed `size`. tests/server/onLoadDocument.ts (7 wire tests) and onStoreDocument.ts (10 wire tests) move from skip to partial — extension-instance and in-process-API tests stay excluded via --match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- New EngineStats (lock-free atomics on hocuspocus-core): inbound messages by type, outbound frames, store outcomes (success/failure/skipped-on-lock), auth denials; absorbs the existing document-connection gauge. Counted in the socket task, document actor, and the axum transport writer; embedders read Engine::stats(). - /metrics renders the counters alongside the gauges (verified end-to-end against a live provider session). - rust.yml: nightly-scheduled ~1h fuzz soak over the three protocol decoders (20 min each), corpora seeded from the TS golden fixtures, crash artifacts uploaded; conformance job gains a 30-minute timeout. - Conformance: lifecycle webhook events are now opt-in per file via rust-target.json `env` (HOCUSPOCUS_RUST_FORCE_EVENTS for onDisconnect.ts's server.configure tests) instead of on for every bare server — the connect-event round trip was inflating hasUnsyncedChanges' flake rate under batch load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…ne, migration guide - crates/npm/: @hocuspocus/server-rust launcher (bin shim resolving the platform binary, HOCUSPOCUS_RUST_BIN override, musl detection) plus six platform-binary packages wired as optionalDependencies (the esbuild distribution pattern). Verified end-to-end against a local build. - crates/Dockerfile: multi-stage musl build on Alpine → minimal non-root runtime with /healthz healthcheck; linux/amd64 + linux/arm64. - .github/workflows/rust-release.yml: on server-rust-v* tags — six-platform binary matrix, packed GitHub release assets, npm publish (platform packages then launcher, versions stamped from the tag), multi-arch GHCR image via buildx. - crates/MIGRATION.md: Node→Rust config mapping, hooks→webhook contract table, extension mapping, zero-downtime mixed-fleet rollout via the interoperable Redis integration, transformer sidecar example, ops notes. - hocuspocus-webhook: reqwest gains rustls-tls — https webhook URLs previously had no TLS backend at all and failed at runtime. - RFC milestone table updated: M5/M6 done. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Mounts the real HocuspocusProviderWebsocketComponent + HocuspocusRoom in jsdom, asserts the room syncs, that an independent plain provider observes a change made through the React-managed document, and that unmounting closes the server-side connection. provider-react previously had no test coverage against either server implementation. Adds jsdom/react/react-dom/tsx as tests devDependencies (react pinned to the instance provider-react resolves, to avoid a duplicate-React tree). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
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.
deleted