Fix explicit relayfile mount layout contract#243
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
CodeAnt AI is reviewing your PR. |
|
Lost in the diff? Review this PR in Change Stack to follow the change map from intent to exact ranges. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (26)
✅ Files skipped from review due to trivial changes (23)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThis PR adds configurable mount local layout ( ChangesMount Layout and Sync Mode Configuration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Relayfile Eval ReviewRun: Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0 Human Review CasesNo reviewable human-review cases captured Relayfile output. |
| outputBuffer, | ||
| input, | ||
| localDir, | ||
| localDir: mountLocalDir, |
There was a problem hiding this comment.
Suggestion: RelayfileMountProcessInstance is being initialized with the already-resolved scoped mount directory, but status() later calls readMountedWorkspaceStatus() which resolves the scoped path again from remotePath and localLayout. In scoped mode this doubles the path segments (for example .../slack/channels/C123/slack/channels/C123), so .relay/state.json is read from the wrong location and readiness/status can fail or fall back to unnecessary HTTP probing. Keep the instance localDir as the original root RELAYFILE_LOCAL_DIR (or stop re-resolving inside status reads) so path resolution happens exactly once. [incorrect variable usage]
Severity Level: Critical 🚨
- ❌ Scoped background mounts ignore existing `.relay/state.json` files.
- ❌ Ready polling for scoped mounts always hits remote HTTP API.
- ⚠️ Scoped mounts may appear unready if HTTP probing fails.
- ⚠️ Increased latency for status checks on scoped layout mounts.Steps of Reproduction ✅
1. In a Node/CLI consumer, construct a `RelayfileSetup` from `@relayfile/sdk/cli` and call
`mountWorkspace()` with a scoped layout, e.g. `remotePath: "/slack/channels/C123"`,
`localDir: "/tmp/mirror"`, `localLayout: "scoped"`, letting `background` default to `true`
(see `mountWorkspace()` in `packages/sdk/typescript/src/setup.ts:12-44` and the
env-contract test in `packages/sdk/typescript/src/setup.test.ts:1250-35`).
2. `mountWorkspace()` normalizes the input and creates a mount session, then builds the
launcher env via `buildMountLauncherEnv()` (`packages/sdk/typescript/src/setup.ts:37-44`),
which sets `RELAYFILE_LOCAL_DIR=/tmp/mirror`,
`RELAYFILE_REMOTE_PATH=/slack/channels/C123`, and `RELAYFILE_MOUNT_LOCAL_LAYOUT=scoped`
before calling `launcher.start({ env })` (see `packages/sdk/typescript/src/setup.ts:37-43`
and `createDefaultMountLauncher()` in
`packages/sdk/typescript/src/mount-launcher.ts:63-71`).
3. Inside `startRelayfileMount()`
(`packages/sdk/typescript/src/mount-launcher.ts:104-149`), `localDir` is resolved from
`RELAYFILE_LOCAL_DIR`, then `mountLocalDir = resolveMountLocalDir(localDir,
env.RELAYFILE_REMOTE_PATH, env.RELAYFILE_MOUNT_LOCAL_LAYOUT)` is computed; in scoped mode
this yields `/tmp/mirror/slack/channels/C123` (see `resolveMountLocalDir()` in
`packages/sdk/typescript/src/mount-launcher.ts:382-395`). The
`RelayfileMountProcessInstance` is then constructed with `localDir: mountLocalDir` (line
145 in the PR hunk), so `this.localDir` becomes `/tmp/mirror/slack/channels/C123`, and the
mount process writes `.relay/state.json` under that directory.
4. When readiness is polled, `RelayfileMountProcessInstance.waitForReady()`
(`packages/sdk/typescript/src/mount-launcher.ts:219-253`) repeatedly calls
`this.status()`, which in turn calls `readMountedWorkspaceStatus({ localDir:
this.localDir, remotePath: env.RELAYFILE_REMOTE_PATH, localLayout:
normalizeMountLocalLayout(env.RELAYFILE_MOUNT_LOCAL_LAYOUT), ... })`
(`packages/sdk/typescript/src/mount-launcher.ts:196-209`). `readMountedWorkspaceStatus()`
immediately calls `resolveMountLocalDir(input.localDir, input.remotePath,
input.localLayout)` (`packages/sdk/typescript/src/mount-launcher.ts:73-78`), treating
`this.localDir` (already `/tmp/mirror/slack/channels/C123`) as the root and
`localLayout="scoped"`, so it returns
`/tmp/mirror/slack/channels/C123/slack/channels/C123`. `readMountStateFile()` then looks
for `.relay/state.json` under this doubled path
(`packages/sdk/typescript/src/mount-launcher.ts:335-343`), fails to find the file, and
returns `null`, causing `readMountedWorkspaceStatus()` to fall back to
`probeMountedWorkspace()` (HTTP `listTree` call at
`packages/sdk/typescript/src/mount-launcher.ts:317-333`). As a result, for any scoped
background mount, local state files are never read; readiness and subsequent
`MountedWorkspaceHandle.status()` calls for those mounts depend solely on HTTP probing and
can fail or timeout even though a valid `.relay/state.json` exists at the first-level
scoped path.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** packages/sdk/typescript/src/mount-launcher.ts
**Line:** 145:145
**Comment:**
*Incorrect Variable Usage: `RelayfileMountProcessInstance` is being initialized with the already-resolved scoped mount directory, but `status()` later calls `readMountedWorkspaceStatus()` which resolves the scoped path again from `remotePath` and `localLayout`. In scoped mode this doubles the path segments (for example `.../slack/channels/C123/slack/channels/C123`), so `.relay/state.json` is read from the wrong location and readiness/status can fail or fall back to unnecessary HTTP probing. Keep the instance `localDir` as the original root `RELAYFILE_LOCAL_DIR` (or stop re-resolving inside status reads) so path resolution happens exactly once.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| if cfg.localLayout == localLayoutScoped { | ||
| return runScopedPollingMountsWithRunner(rootCtx, cfg, remotePaths, run) | ||
| } | ||
| return runSinglePollingMount(rootCtx, cfg) | ||
| if len(remotePaths) > 1 { | ||
| return fmt.Errorf("multiple --remote-path values require --local-layout=%s", localLayoutScoped) | ||
| } | ||
| cfg.remotePath = normalizeMountRemotePath(remotePaths[0]) | ||
| cfg.remotePaths = nil | ||
| return run(rootCtx, cfg) |
There was a problem hiding this comment.
Suggestion: The multi---remote-path/--local-layout=scoped contract is enforced only inside the polling runner, so --mode=fuse bypasses this validation and silently drops extra remote paths (the FUSE path uses only the single remotePath). Move this validation to config setup or executeMount so both poll and fuse modes enforce the same contract and reject invalid multi-path exact layouts consistently. [api mismatch]
Severity Level: Major ⚠️
- ❌ FUSE mode relayfile-mount ignores extra --remote-path roots.
- ⚠️ Users cannot detect misconfigured multi-root mounts at startup.Steps of Reproduction ✅
1. Build the `relayfile-mount` binary with FUSE support so that `defaultFuseRunner` is set
to `runFuseMount` via the `init()` function in `cmd/relayfile-mount/fuse_mount.go:15-17`.
2. Run the binary with FUSE mode and multiple remote paths but without
`--local-layout=scoped`, for example:
`relayfile-mount --mode=fuse --workspace=... --token=... --local-dir=/mnt/test
--remote-path=/a --remote-path=/b`
(flags parsed in `main()` at `cmd/relayfile-mount/main.go:75-104`).
3. In `main()` (`cmd/relayfile-mount/main.go:122-172`), `allRemotePaths` is built from the
repeated `--remote-path` flag, `firstRemotePath()` (`main.go:81-87`) sets `cfg.remotePath`
to the first normalized path (`/a`), and `normalizeRemotePaths()` (`main.go:89-107`) sets
`cfg.remotePaths` to the full deduplicated list (`[/a, /b]`). `cfg.localLayout` is set to
`localLayoutExact` by `resolveLocalLayout()` (`main.go:198-208`), and `cfg.mode` is set to
`mountModeFuse` by `resolveMountMode()` (`main.go:182-195`).
4. `executeMount()` (`cmd/relayfile-mount/main.go:224-232`) is called with this `cfg`.
Because `cfg.mode == mountModeFuse`, it calls `runFuseMount()`
(`cmd/relayfile-mount/fuse_mount.go:19-60`) directly, never invoking
`runPollingMountWithRunner()` (`main.go:239-252`) where the
multi-`remotePaths`/`localLayout` validation lives. Inside `runFuseMount()`, only
`cfg.remotePath` is used as `RemoteRoot` (`fuse_mount.go:25-29`); `cfg.remotePaths` and
`cfg.localLayout` are ignored. The FUSE mount at `cfg.localDir` therefore exposes only
`/a`, silently dropping `/b` without any error, even though polling mode would reject the
same multi-path exact layout as invalid.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** cmd/relayfile-mount/main.go
**Line:** 244:252
**Comment:**
*Api Mismatch: The multi-`--remote-path`/`--local-layout=scoped` contract is enforced only inside the polling runner, so `--mode=fuse` bypasses this validation and silently drops extra remote paths (the FUSE path uses only the single `remotePath`). Move this validation to config setup or `executeMount` so both poll and fuse modes enforce the same contract and reject invalid multi-path exact layouts consistently.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| @@ -190,6 +199,8 @@ class RelayfileMountProcessInstance implements MountLauncherInstance { | |||
| workspaceId: this.input.env.RELAYFILE_WORKSPACE ?? "", | |||
| remotePath: this.input.env.RELAYFILE_REMOTE_PATH ?? "/", | |||
| mode: normalizeMountMode(this.input.env.RELAYFILE_MOUNT_MODE) ?? "poll", | |||
| localLayout: normalizeMountLocalLayout(this.input.env.RELAYFILE_MOUNT_LOCAL_LAYOUT), | |||
| syncMode: normalizeMountSyncMode(this.input.env.RELAYFILE_MOUNT_SYNC_MODE), | |||
There was a problem hiding this comment.
🟠 Architect Review — HIGH
Scoped mounts double-resolve the local mount root when polling status from a launcher instance: startRelayfileMount() resolves a scoped mountLocalDir and stores it as this.localDir, but RelayfileMountProcessInstance.status() passes that already-resolved path back into readMountedWorkspaceStatus(), which again applies resolveMountLocalDir() using the same remotePath/localLayout. For scoped layouts this produces a non-existent nested path (e.g. <root>/slack/channels/C123/slack/channels/C123/.relay/state.json), so instance readiness polling ignores the real .relay/state.json and falls back to remote probing, giving incorrect ready/timeout behavior for normal scoped launches.
Suggestion: Keep a single canonical contract for localDir in status calls (either always pass the mirror root and let readMountedWorkspaceStatus() resolve once, or always pass the resolved mount root and skip re-resolution), and add an end-to-end launcher test for localLayout: "scoped" that asserts instance.ready/status() reads the actual scoped .relay/state.json path.
Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is an **Architect / Logical Review** comment left during a code review. These reviews are first-class, important findings — not optional suggestions. Do NOT dismiss this as a 'big architectural change' just because the title says architect review; most of these can be resolved with a small, localized fix once the intent is understood.
**Path:** packages/sdk/typescript/src/mount-launcher.ts
**Line:** 145:203
**Comment:**
*HIGH: Scoped mounts double-resolve the local mount root when polling status from a launcher instance: startRelayfileMount() resolves a scoped mountLocalDir and stores it as this.localDir, but RelayfileMountProcessInstance.status() passes that already-resolved path back into readMountedWorkspaceStatus(), which again applies resolveMountLocalDir() using the same remotePath/localLayout. For scoped layouts this produces a non-existent nested path (e.g. `<root>/slack/channels/C123/slack/channels/C123/.relay/state.json`), so instance readiness polling ignores the real `.relay/state.json` and falls back to remote probing, giving incorrect ready/timeout behavior for normal scoped launches.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
If a suggested approach is provided above, use it as the authoritative instruction. If no explicit code suggestion is given, you MUST still draft and apply your own minimal, localized fix — do not punt back with 'no suggestion provided, review manually'. Keep the change as small as possible: add a guard clause, gate on a loading state, reorder an await, wrap in a conditional, etc. Do not refactor surrounding code or expand scope beyond the finding.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| @@ -2000,7 +2015,11 @@ func (s *Syncer) sync(ctx context.Context, forcePoll bool) error { | |||
|
|
|||
| conflicted := map[string]struct{}{} | |||
| didPoll := false | |||
| if !s.state.BootstrapComplete || s.forceFullReconcile { | |||
| if s.writeOnly { | |||
| if !s.state.BootstrapComplete { | |||
| s.markBootstrapComplete() | |||
| } | |||
| } else if !s.state.BootstrapComplete || s.forceFullReconcile { | |||
| if err := s.pullRemote(ctx, conflicted); err != nil { | |||
| s.markSyncError(err) | |||
| _ = s.saveState() | |||
| @@ -2018,7 +2037,7 @@ func (s *Syncer) sync(ctx context.Context, forcePoll bool) error { | |||
| } | |||
|
|
|||
| shouldPoll := !didPoll && (forcePoll || !s.bootstrapped || s.wsConn == nil) | |||
| if shouldPoll { | |||
| if shouldPoll && !s.writeOnly { | |||
| if err := s.pullRemote(ctx, conflicted); err != nil { | |||
There was a problem hiding this comment.
🟠 Architect Review — HIGH
The write-only sync mode only guards remote pulls inside Syncer.sync() (skipping MaintainWebSocket and pullRemote there), but websocket event ingestion is still driven externally: both cmd/relayfile-mount and cmd/relayfile-cli tickers call syncer.MaintainWebSocket() whenever websocketEnabled is true, regardless of s.writeOnly. In write-only mode this still establishes a websocket, receives events, calls ReadFile, and applies remote files via applyWebSocketEvent, violating the "no remote history/event pulls" contract in normal daemon operation.
Suggestion: Enforce write-only at the websocket layer as well by preventing MaintainWebSocket/connectWebSocket from running when Syncer.writeOnly is true (and/or gating the wsTicker callers on syncMode), and add an integration test that runs a write-only daemon with websocketEnabled=true to prove that no event-driven ReadFile/applyRemoteFile paths execute.
Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is an **Architect / Logical Review** comment left during a code review. These reviews are first-class, important findings — not optional suggestions. Do NOT dismiss this as a 'big architectural change' just because the title says architect review; most of these can be resolved with a small, localized fix once the intent is understood.
**Path:** internal/mountsync/syncer.go
**Line:** 1995:2041
**Comment:**
*HIGH: The write-only sync mode only guards remote pulls inside Syncer.sync() (skipping MaintainWebSocket and pullRemote there), but websocket event ingestion is still driven externally: both cmd/relayfile-mount and cmd/relayfile-cli tickers call syncer.MaintainWebSocket() whenever websocketEnabled is true, regardless of s.writeOnly. In write-only mode this still establishes a websocket, receives events, calls ReadFile, and applies remote files via applyWebSocketEvent, violating the "no remote history/event pulls" contract in normal daemon operation.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
If a suggested approach is provided above, use it as the authoritative instruction. If no explicit code suggestion is given, you MUST still draft and apply your own minimal, localized fix — do not punt back with 'no suggestion provided, review manually'. Keep the change as small as possible: add a guard clause, gate on a loading state, reorder an await, wrap in a conditional, etc. Do not refactor surrounding code or expand scope beyond the finding.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
CodeAnt AI finished reviewing your PR. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/sdk/typescript/src/setup.ts (1)
1293-1294: 💤 Low valueConsider refactoring to avoid setting values that are immediately overridden.
Lines 1293-1294 set
localLayoutandsyncModeto their default constants, butcreateMountSession(lines 509-513) immediately spreads the session and overrides these fields withinput.localLayoutandinput.syncMode. The defaults here serve only to satisfy theMountSessionResultreturn type.Consider changing
validateMountSessionResponseto returnOmit<MountSessionResult, 'localLayout' | 'syncMode'>, then removing lines 1293-1294, so the caller is responsible for merging in the local-only fields. This would make it clearer that these fields don't come from the cloud API.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/sdk/typescript/src/setup.ts` around lines 1293 - 1294, The current code sets localLayout and syncMode to DEFAULT_MOUNT_LOCAL_LAYOUT/DEFAULT_MOUNT_SYNC_MODE only to have them immediately overridden by createMountSession; change validateMountSessionResponse to return Omit<MountSessionResult, 'localLayout' | 'syncMode'> (i.e. remove those two fields from the validated/returned type), remove the default assignments of localLayout and syncMode where DEFAULT_MOUNT_LOCAL_LAYOUT and DEFAULT_MOUNT_SYNC_MODE are used, and ensure createMountSession is responsible for merging in input.localLayout and input.syncMode into the final MountSessionResult; update any type annotations and callers accordingly to reflect MountSessionResult now being composed by combining the API response (from validateMountSessionResponse) with the local-only fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/mountsync/syncer.go`:
- Around line 2018-2022: The code currently sets s.state.BootstrapComplete when
s.writeOnly is true (via s.markBootstrapComplete), which causes write-only
mounts to later take the fast restart path and miss remote files; instead, do
NOT persist the mirror BootstrapComplete flag for write-only mounts — remove or
bypass the s.markBootstrapComplete call when s.writeOnly is true and explicitly
clear any write-only bootstrap/incremental markers used by the mount (use
dedicated flags instead of s.state.BootstrapComplete). Update the logic around
s.writeOnly, s.state.BootstrapComplete, s.markBootstrapComplete and the
restart/fast-path checks (the code referenced around the forceFullReconcile and
the later restart section) so write-only mounts never record or rely on
BootstrapComplete and instead use/clear a separate write-only state marker.
---
Nitpick comments:
In `@packages/sdk/typescript/src/setup.ts`:
- Around line 1293-1294: The current code sets localLayout and syncMode to
DEFAULT_MOUNT_LOCAL_LAYOUT/DEFAULT_MOUNT_SYNC_MODE only to have them immediately
overridden by createMountSession; change validateMountSessionResponse to return
Omit<MountSessionResult, 'localLayout' | 'syncMode'> (i.e. remove those two
fields from the validated/returned type), remove the default assignments of
localLayout and syncMode where DEFAULT_MOUNT_LOCAL_LAYOUT and
DEFAULT_MOUNT_SYNC_MODE are used, and ensure createMountSession is responsible
for merging in input.localLayout and input.syncMode into the final
MountSessionResult; update any type annotations and callers accordingly to
reflect MountSessionResult now being composed by combining the API response
(from validateMountSessionResponse) with the local-only fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4b4da029-873f-4131-8743-5a9c79a5b0fa
📒 Files selected for processing (10)
cmd/relayfile-mount/main.gocmd/relayfile-mount/main_test.gointernal/mountsync/syncer.gointernal/mountsync/syncer_test.gopackages/sdk/typescript/src/index.tspackages/sdk/typescript/src/mount-launcher.test.tspackages/sdk/typescript/src/mount-launcher.tspackages/sdk/typescript/src/setup-types.tspackages/sdk/typescript/src/setup.test.tspackages/sdk/typescript/src/setup.ts
| if s.writeOnly { | ||
| if !s.state.BootstrapComplete { | ||
| s.markBootstrapComplete() | ||
| } | ||
| } else if !s.state.BootstrapComplete || s.forceFullReconcile { |
There was a problem hiding this comment.
Don't persist BootstrapComplete for write-only mounts.
Line 2020 records the private state as fully bootstrapped even though write-only never mirrored remote history. On a later mirror start, Lines 2542-2560 trust that bit and take the restart fast-path, which can skip the initial full pull and leave pre-existing remote files missing locally until a new event arrives or the periodic fallback runs. Clear the write-only bootstrap/incremental markers explicitly instead of reusing the mirror-bootstrap flag here.
Also applies to: 2542-2560
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/mountsync/syncer.go` around lines 2018 - 2022, The code currently
sets s.state.BootstrapComplete when s.writeOnly is true (via
s.markBootstrapComplete), which causes write-only mounts to later take the fast
restart path and miss remote files; instead, do NOT persist the mirror
BootstrapComplete flag for write-only mounts — remove or bypass the
s.markBootstrapComplete call when s.writeOnly is true and explicitly clear any
write-only bootstrap/incremental markers used by the mount (use dedicated flags
instead of s.state.BootstrapComplete). Update the logic around s.writeOnly,
s.state.BootstrapComplete, s.markBootstrapComplete and the restart/fast-path
checks (the code referenced around the forceFullReconcile and the later restart
section) so write-only mounts never record or rely on BootstrapComplete and
instead use/clear a separate write-only state marker.
Review verdict: APPROVE (bound to cf7fd17)Reviewer: claude-mount-cleanup (independent reviewer; author codex-mount-fix; posted as comment because both agents share the repo's GitHub identity, so the formal Approve button self-rejects). Scope-conformance first, then correctness, per run charter. Scope-conformance — 7/7 ledger items pass
Correctness — independently verified
Non-blocking notes
🤖 Generated with Claude Code |
There was a problem hiding this comment.
4 issues found across 10 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/mountsync/syncer.go">
<violation number="1" location="internal/mountsync/syncer.go:1995">
P1: Write-only mode is not fully enforced: external websocket maintenance can still pull and apply remote events.</violation>
<violation number="2" location="internal/mountsync/syncer.go:2019">
P1: Write-only mounts should not persist `BootstrapComplete` in shared state. If this same state directory is later used by a mirror-mode restart, the restart fast-path trusts the bootstrap flag and skips the initial full pull, leaving pre-existing remote files missing locally until a new event arrives or the periodic reconcile runs. Consider using a separate marker (e.g., `WriteOnlyInitialized`) or clearing mirror-related bootstrap/incremental markers explicitly.</violation>
</file>
<file name="packages/sdk/typescript/src/mount-launcher.ts">
<violation number="1" location="packages/sdk/typescript/src/mount-launcher.ts:145">
P2: Scoped mounts can resolve the state path twice, causing status checks to miss `.relay/state.json` and use fallback probing.</violation>
</file>
<file name="cmd/relayfile-mount/main.go">
<violation number="1" location="cmd/relayfile-mount/main.go:247">
P2: Multi-path validation is only enforced inside `runPollingMountWithRunner`, so FUSE mode (`--mode=fuse`) bypasses this check entirely. A user passing multiple `--remote-path` values with `--mode=fuse` will silently get only the first path mounted. Move this validation to `executeMount` or the config setup phase so both poll and FUSE modes reject invalid multi-path exact layouts consistently at startup.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| if err := s.MaintainWebSocket(ctx); err != nil { | ||
| s.logf("websocket unavailable; using polling sync: %v", err) | ||
| if !s.writeOnly { |
There was a problem hiding this comment.
P1: Write-only mode is not fully enforced: external websocket maintenance can still pull and apply remote events.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/mountsync/syncer.go, line 1995:
<comment>Write-only mode is not fully enforced: external websocket maintenance can still pull and apply remote events.</comment>
<file context>
@@ -1979,8 +1992,10 @@ func (s *Syncer) sync(ctx context.Context, forcePoll bool) error {
- if err := s.MaintainWebSocket(ctx); err != nil {
- s.logf("websocket unavailable; using polling sync: %v", err)
+ if !s.writeOnly {
+ if err := s.MaintainWebSocket(ctx); err != nil {
+ s.logf("websocket unavailable; using polling sync: %v", err)
</file context>
| didPoll := false | ||
| if !s.state.BootstrapComplete || s.forceFullReconcile { | ||
| if s.writeOnly { | ||
| if !s.state.BootstrapComplete { |
There was a problem hiding this comment.
P1: Write-only mounts should not persist BootstrapComplete in shared state. If this same state directory is later used by a mirror-mode restart, the restart fast-path trusts the bootstrap flag and skips the initial full pull, leaving pre-existing remote files missing locally until a new event arrives or the periodic reconcile runs. Consider using a separate marker (e.g., WriteOnlyInitialized) or clearing mirror-related bootstrap/incremental markers explicitly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/mountsync/syncer.go, line 2019:
<comment>Write-only mounts should not persist `BootstrapComplete` in shared state. If this same state directory is later used by a mirror-mode restart, the restart fast-path trusts the bootstrap flag and skips the initial full pull, leaving pre-existing remote files missing locally until a new event arrives or the periodic reconcile runs. Consider using a separate marker (e.g., `WriteOnlyInitialized`) or clearing mirror-related bootstrap/incremental markers explicitly.</comment>
<file context>
@@ -2000,7 +2015,11 @@ func (s *Syncer) sync(ctx context.Context, forcePoll bool) error {
didPoll := false
- if !s.state.BootstrapComplete || s.forceFullReconcile {
+ if s.writeOnly {
+ if !s.state.BootstrapComplete {
+ s.markBootstrapComplete()
+ }
</file context>
| outputBuffer, | ||
| input, | ||
| localDir, | ||
| localDir: mountLocalDir, |
There was a problem hiding this comment.
P2: Scoped mounts can resolve the state path twice, causing status checks to miss .relay/state.json and use fallback probing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/typescript/src/mount-launcher.ts, line 145:
<comment>Scoped mounts can resolve the state path twice, causing status checks to miss `.relay/state.json` and use fallback probing.</comment>
<file context>
@@ -133,7 +142,7 @@ async function startRelayfileMount(
outputBuffer,
input,
- localDir,
+ localDir: mountLocalDir,
now: options.now ?? Date.now,
readyPollIntervalMs:
</file context>
| localDir: mountLocalDir, | |
| localDir, |
| return runScopedPollingMountsWithRunner(rootCtx, cfg, remotePaths, run) | ||
| } | ||
| return runSinglePollingMount(rootCtx, cfg) | ||
| if len(remotePaths) > 1 { |
There was a problem hiding this comment.
P2: Multi-path validation is only enforced inside runPollingMountWithRunner, so FUSE mode (--mode=fuse) bypasses this check entirely. A user passing multiple --remote-path values with --mode=fuse will silently get only the first path mounted. Move this validation to executeMount or the config setup phase so both poll and FUSE modes reject invalid multi-path exact layouts consistently at startup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/relayfile-mount/main.go, line 247:
<comment>Multi-path validation is only enforced inside `runPollingMountWithRunner`, so FUSE mode (`--mode=fuse`) bypasses this check entirely. A user passing multiple `--remote-path` values with `--mode=fuse` will silently get only the first path mounted. Move this validation to `executeMount` or the config setup phase so both poll and FUSE modes reject invalid multi-path exact layouts consistently at startup.</comment>
<file context>
@@ -189,14 +233,23 @@ func executeMount(rootCtx context.Context, cfg mountConfig, runPoll pollRunner,
+ return runScopedPollingMountsWithRunner(rootCtx, cfg, remotePaths, run)
}
- return runSinglePollingMount(rootCtx, cfg)
+ if len(remotePaths) > 1 {
+ return fmt.Errorf("multiple --remote-path values require --local-layout=%s", localLayoutScoped)
+ }
</file context>
|
Reviewed PR #243 and made one fix: Validated stale bot findings against current checkout; the referenced SDK/core/mountfuse issues were already resolved or not applicable to this PR’s current files. Verification passed:
|
|
✅ pr-reviewer applied fixes — committed and pushed Reviewed PR #243 and made one fix: Validated stale bot findings against current checkout; the referenced SDK/core/mountfuse issues were already resolved or not applicable to this PR’s current files. Verification passed:
|
There was a problem hiding this comment.
1 issue found across 26 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".trajectories/index.json">
<violation number="1" location=".trajectories/index.json:178">
P1: All trajectory `path` values use machine-specific absolute paths (`/home/daytona/workspace/...`) instead of portable repo-relative paths (`.trajectories/...`). The `trail` CLI tool resolves these paths to locate trajectory JSON files. Absolute paths hard-code a specific developer's machine home, breaking path resolution for all other contributors, CI, and any environment where the repo is checked out at a different location.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @@ -1,289 +1,181 @@ | |||
| { | |||
There was a problem hiding this comment.
P1: All trajectory path values use machine-specific absolute paths (/home/daytona/workspace/...) instead of portable repo-relative paths (.trajectories/...). The trail CLI tool resolves these paths to locate trajectory JSON files. Absolute paths hard-code a specific developer's machine home, breaking path resolution for all other contributors, CI, and any environment where the repo is checked out at a different location.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .trajectories/index.json, line 178:
<comment>All trajectory `path` values use machine-specific absolute paths (`/home/daytona/workspace/...`) instead of portable repo-relative paths (`.trajectories/...`). The `trail` CLI tool resolves these paths to locate trajectory JSON files. Absolute paths hard-code a specific developer's machine home, breaking path resolution for all other contributors, CI, and any environment where the repo is checked out at a different location.</comment>
<file context>
@@ -1,289 +1,181 @@
- "path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-issue-214/.trajectories/completed/2026-05/traj_0meitjjmvpf2.json"
+ "startedAt": "2026-06-06T00:23:06.923Z",
+ "completedAt": "2026-06-06T00:23:16.097Z",
+ "path": "/home/daytona/workspace/.trajectories/completed/2026-06/traj_cf89ajbo2ast.json"
}
}
</file context>
Delta re-review: APPROVE re-affirmed at eb23e7f (range cf7fd17..eb23e7f)Reviewer: claude-mount-cleanup. Bot-push delta ( Code delta (independently verified:
Clean verification at eb23e7f: Approval of record now bound to eb23e7f. Any further push voids it again. 🤖 Generated with Claude Code |
|
Implemented and verified the PR repair pass. Fixed:
Local validation passed:
|
|
Blocked files: Implemented and verified the PR repair pass. Fixed:
Local validation passed:
|
|
Fixed a validated scoped-layout readiness bug in mount-launcher.ts: launcher instances already store the resolved scoped mount root, so Added a regression in mount-launcher.test.ts covering Verified locally:
|
|
Blocked files: Fixed a validated scoped-layout readiness bug in mount-launcher.ts: launcher instances already store the resolved scoped mount root, so Added a regression in mount-launcher.test.ts covering Verified locally:
|
|
Fixed a scoped mount launcher regression in mount-launcher.ts: launcher instances already store the resolved mount root, so status now reads it as Added a regression test in mount-launcher.test.ts proving scoped launcher readiness comes from the resolved Validated: Could not run Go tests because |
|
Blocked files: Fixed a scoped mount launcher regression in mount-launcher.ts: launcher instances already store the resolved mount root, so status now reads it as Added a regression test in mount-launcher.test.ts proving scoped launcher readiness comes from the resolved Validated: Could not run Go tests because |
|
Reviewed PR #243 and fixed one validated gap: Verified:
No bot review artifacts were present beyond |
|
Blocked files: Reviewed PR #243 and fixed one validated gap: Verified:
No bot review artifacts were present beyond |
|
Fixed two validated PR issues:
Verified:
|
|
Blocked files: Fixed two validated PR issues:
Verified:
|
Summary
--local-layout/RELAYFILE_MOUNT_LOCAL_LAYOUTcontract torelayfile-mount:exactis the default and preserves the legacy single-path contract:--local-diris the mirror root, even for a non-root--remote-path.scopedis opt-in and appends each remote path under the local root for multi/scoped mounts.--local-layout=scopedinstead of relying on count-based inference.--sync-mode/RELAYFILE_MOUNT_SYNC_MODEwithmirrordefault andwrite-onlymode for canonical writeback roots that must push local drafts without pulling provider history..relay/state.jsonand creating.relay/dead-letter/for writeback feedback, while skipping remote tree/event pulls..relay/state.json, logs, pid files, and cwd from the same explicit layout contract.mount layout=<layout> remote=<remote> local=<local> sync=<sync> mode=<mode> state=<path>.Restart / stale-state note
The fixed binary writes private per-mount state under
RELAYFILE_MOUNT_STATE_DIR/~/.relayfile-mount-state/<mount-id>/state.jsonand rewrites the public.relay/state.jsonat the resolved mount root. For the #206 cleanup runbook: after stopping the old PIDs, wipe the doubled nested roots and legacy.relayfile-mount-state.json*files at the mount roots, but do not delete canonical content roots. Existing.relay/state.jsonfiles are tolerated and refreshed by the new mount.Out of scope
Delivered draft residue / rename-on-ack is a separate service-side issue tracked in #242.
Tests
go test ./cmd/relayfile-mount ./internal/mountsyncnpm run build --workspace=packages/corenpm run typecheck --workspace=packages/sdk/typescriptnpm run test --workspace=packages/sdk/typescript -- --run src/mount-launcher.test.ts src/setup.test.tsRelated