Skip to content

SDK/mount: content-hash client cache (If-None-Match) + jittered Retry-After on all 429 paths - #519

Merged
khaliqgant merged 6 commits into
mainfrom
relayflow/relayfile-garden-relayfile-2-9119454f-ad4ae9cb
Sep 28, 2026
Merged

khaliqgant merged 6 commits into
mainfrom
relayflow/relayfile-garden-relayfile-2-9119454f-ad4ae9cb

Conversation

@agent-relay-code

@agent-relay-code agent-relay-code Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

PR summary

What changed

  • Replaced the TypeScript path/TTL cache with a configurable 32 MiB byte-capped content-addressed LRU, and added equivalent sync/async Python caching. Cached reads send If-None-Match using contentHash and serve 304 responses locally; caching can be disabled.
  • Added a verified, atomic, permission-restricted mount object store at ~/.relayfile/cache/objects/<sha256>. Authorized bootstrap tree hashes are checked before body reads, so later mounts can materialize identical content without transferring it again.
  • Reduced bootstrap and incremental point-read concurrency from 16 (previous environment ceiling 64) to a hard ceiling of 4.
  • Changed SDK and Go transport retry delay calculation to full jitter while treating Retry-After as a minimum, including HTTP-date parsing already supported by each client.
  • Made relayfile listen retry initial 429/503 handshakes, retain the last processed event cursor, and reconnect from that cursor with full jitter. Applied the same handshake Retry-After/jitter behavior to mount and FUSE WebSocket reconnects.
  • Added overload details.reason propagation to Go HTTPError, updated SDK parity metadata, tests, and configuration/changelog documentation.

Scope decisions

  • Included internal/mountfuse/wsinvalidate.go because it is a shipped /fs/ws reconnect loop with the same fleet lockstep risk.
  • Deferred SDK setup/control-plane retries, packages/agents reconnects, mount outbox writes, and CLI polite polling: they are one-shot, write-path, or polling flows rather than the file-read fan-in and event reconnect paths addressed here.
  • No server handler or provider mutation path changed. The digest runtime contract therefore does not apply; filesystem-event emission and digest regeneration behavior are unchanged.

Validation

  • TypeScript SDK build and typecheck pass. All 113 relevant client.test.ts assertions pass; one pre-existing environment assertion expects Node to lack global ErrorEvent, which is false on the installed Node 25 runtime.
  • Python SDK: 97 tests pass.
  • Targeted Go cache/retry/WebSocket/listen tests pass for internal/mountsync, internal/mountfuse, and cmd/relayfile-cli.
  • scripts/check-contract-surface.sh passes, including SDK parity.
  • The complete TypeScript package suite additionally requires the downloaded Go toolchain to be on the subprocess PATH; unrelated launcher timing tests remain environment-sensitive.

Contract note

The clients deliberately use the response body's contentHash for object identity. The currently documented server ETag is a revision identifier; the separate server change must redefine it to the quoted content hash and add If-None-Match/304 handling before conditional requests can save network bodies against older servers.

Checks

Relayflow ran this repository's checks (.relayflow/check.sh) and they passed.

What ran (.relayflow/check.sh)
#!/bin/sh
set -e

# Match and provision the toolchains used by CI: Go 1.22, Node.js 22, and Bun 1.3.14.
. /usr/local/share/nvm/nvm.sh
nvm install 22
nvm use 22

TOOLCHAIN_DIR="$PWD/.relayflow/toolchains"
GO_DIR="$TOOLCHAIN_DIR/go-1.22.12"
if [ ! -x "$GO_DIR/bin/go" ]; then
  mkdir -p "$TOOLCHAIN_DIR"
  curl -fsSL https://go.dev/dl/go1.22.12.linux-amd64.tar.gz \
    | tar -xz -C "$TOOLCHAIN_DIR"
  mv "$TOOLCHAIN_DIR/go" "$GO_DIR"
fi
PATH="$GO_DIR/bin:$PATH"
export PATH

BUN_INSTALL="$TOOLCHAIN_DIR/bun-1.3.14"
export BUN_INSTALL
if [ ! -x "$BUN_INSTALL/bin/bun" ]; then
  curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.14
fi
PATH="$BUN_INSTALL/bin:$PATH"
export PATH

npm ci
go mod download

# Run generated-source checks and builds before the test suites, as CI does.
npm run codegen --workspace=@relayfile/client
git diff --exit-code -- packages/client/src/generated/control-plane.ts
npm run build
mkdir -p bin
go build -o bin/relayfile ./cmd/relayfile
go build -o bin/relayfile-mount ./cmd/relayfile-mount
go build -o bin/relayfile-cli ./cmd/relayfile-cli

# Static and repository contract checks.
npm run typecheck
./scripts/check-contract-surface.sh
node --test scripts/validate-mount-qualification-workflow.test.mjs
node scripts/validate-mount-qualification-workflow.mjs
./scripts/check-publish-workflow.sh
./scripts/check-publish-workflow.sh .github/workflows/publish-python.yml
./scripts/test-check-publish-workflow.sh

# Unit, release-tooling, runtime compatibility, and local E2E tests.
npm test
npm run test:release
npm run test:bundle:bun --workspace=packages/sdk/typescript
chmod +x bin/relayfile bin/relayfile-mount bin/relayfile-cli
CI=true npx tsx scripts/e2e.ts --ci

# Provider-backed evals are omitted because they require OPENROUTER_API_KEY.
# Publish workflows are omitted because they mutate versions, publish packages,
# and require GitHub/PyPI credentials and GitHub Actions artifact services.
# Mount qualification upload/download checks are omitted because they require
# GitHub Actions artifact services; their producer contract is validated above.

Fixes #518

Review in cubic


Note

Medium Risk
Touches default read/retry behavior and mount bootstrap concurrency fleet-wide; correctness depends on ETag/304 server support for SDK conditional reads, though mount object reuse is hash-verified locally.

Overview
Adds content-addressed read caching and polite overload handling across SDKs, mounts, and long-lived WebSocket listeners.

SDKs (TypeScript & Python): Replaces the old TTL/entry-count read cache with a byte-capped LRU keyed by contentHash, with per-path metadata kept separate. Cached reads send the server’s opaque ETag in If-None-Match, handle 304, and skip caching when ETag or contentHash is missing. Retries now use full jitter with Retry-After (and body hints where applicable) as a minimum delay rather than capping it at the client backoff.

Mount sync (internal/mountsync): Introduces a verified on-disk object store at ~/.relayfile/cache/objects/<sha256> (1 GiB LRU, bytes only). Bootstrap/incremental reads skip network bodies when the tree entry’s hash is already local, while path/revision still come from the current mount’s tree. Bootstrap and incremental read concurrency drop from 16 (env max 64) to a hard cap of 4. HTTP errors can surface details.reason on HTTPError.

Reconnect paths: relayfile listen keeps the last event cursor and reconnects with cursor= on the dial URL; initial 429/503 handshakes retry instead of failing immediately. FUSE/mount WebSocket invalidators parse dial Retry-After and use the same jitter pattern. Syncer WebSocket reconnect delay switches to full jitter.

Mount launcher: Foreground --once keeps polling mount readiness until the timeout after exit 0, avoiding a race where state isn’t visible yet.

Docs, parity metadata (client-read-cache → both), changelogs, and tests accompany the behavior changes. Trajectory index JSON under .trajectories/ is also updated.

Reviewed by Cursor Bugbot for commit 6364f3f. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 16ea218e-f66b-4e53-9223-8d0943e58ea8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-09-28T06-26-36-540Z-HEAD-provider
Mode: provider
Git SHA: c060696

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Devin Review

Comment thread packages/sdk/typescript/src/client.ts Outdated
Comment thread packages/sdk/typescript/src/client.ts Outdated
Comment thread internal/mountsync/object_cache.go Outdated
Comment thread internal/mountsync/syncer.go Outdated
Comment thread internal/mountsync/object_cache.go Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/sdk/typescript/src/client.ts Outdated
Comment thread packages/sdk/typescript/src/client.ts Outdated
khaliqgant and others added 2 commits September 28, 2026 07:54
Address review feedback on the client content-hash caches:

- SDK (TS + Python): dedupe only content bytes by (hash, encoding); keep
  revision/path/semantics per cache key so two paths with identical bytes
  never swap metadata on a 304.
- SDK (TS): never store hashless responses; they cannot be revalidated with
  If-None-Match, so they always go to the server instead of being served
  from cache indefinitely.
- mount: the persistent object store now holds raw bytes only (no path,
  revision or content type from the workspace that first fetched them) and
  is byte-capped (1 GiB default) with LRU eviction by mtime.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The server ETag for /fs/file is opaque and may encode more than the content
hash, so building If-None-Match from contentHash never revalidates. Both
SDKs now store the exact ETag the server returned for each path (refreshed
from 304 responses) and echo it verbatim in If-None-Match. The local byte
store stays keyed by the response body's contentHash. Responses without an
ETag are not cached, since they cannot be revalidated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit be39be3. Configure here.

Comment thread packages/sdk/python/src/relayfile/client.py Outdated
put() invalidated the path under the lock, released it to validate the
body, then re-inserted. A concurrent put for the same path could leave a
stale object reference whose later LRU eviction dropped the live path.
Validate outside the lock, then invalidate + insert under one lock hold;
eviction now only drops paths that still point at the evicted object.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@khaliqgant
khaliqgant merged commit c2df5f5 into main Sep 28, 2026
11 checks passed
@khaliqgant
khaliqgant deleted the relayflow/relayfile-garden-relayfile-2-9119454f-ad4ae9cb branch September 28, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SDK/mount: content-hash client cache (If-None-Match) + jittered Retry-After on all 429 paths

1 participant