Skip to content

feat: project one workspace mirror to multiple local views - #411

Merged
khaliqgant merged 2 commits into
mainfrom
fix/409-multi-view-mirror
Aug 8, 2026
Merged

feat: project one workspace mirror to multiple local views#411
khaliqgant merged 2 commits into
mainfrom
fix/409-multi-view-mirror

Conversation

@miyaontherelay

Copy link
Copy Markdown
Contributor

Fixes #409.\n\nKeeps one canonical registered mirror and adds explicit symlink-backed subtree views. The canonical daemon remains the sole owner of local state, watcher, event cursor, and writeback outbox. Workspace health reports every view independently and public state exposes event-listener liveness so an idle listener is distinct from a disconnected one.\n\nVerification: go test ./cmd/relayfile-cli; go test ./internal/mountsync -run '^$'.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kjgbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46fd3309-faca-4506-b86b-7feb265dfdc5

📥 Commits

Reviewing files that changed from the base of the PR and between fe8eae1 and 4b08411.

📒 Files selected for processing (4)
  • cmd/relayfile-cli/main.go
  • cmd/relayfile-cli/main_test.go
  • internal/mountsync/syncer.go
  • internal/mountsync/syncer_test.go
📝 Walkthrough

Walkthrough

Workspace records now support registered symlinked subtree views. Workspace commands manage views and report their health. Sync state now records event-listener status, timestamps, heartbeat, connection attempts, and retry data.

Changes

Workspace views and listener health

Layer / File(s) Summary
Event-listener health telemetry
internal/mountsync/syncer.go
The syncer records websocket timing data, derives listener status, updates heartbeat timestamps, and publishes listener health in state JSON.
Workspace view lifecycle
cmd/relayfile-cli/main.go, cmd/relayfile-cli/main_test.go
Workspace records persist view registrations. Workspace commands create, list, and remove validated symlinks. Tests cover persistence, read/write-through behavior, and canonical mirror preservation.
Workspace health and status reporting
cmd/relayfile-cli/main.go, cmd/relayfile-cli/main_test.go
Workspace health loads listener state, evaluates registered views, reports statuses, and exposes listener data in text and JSON output. Tests cover ready and wrong-target views.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceCLI
  participant CanonicalMirror
  participant Syncer
  participant StateJSON
  WorkspaceCLI->>CanonicalMirror: validate and create workspace view symlink
  WorkspaceCLI->>CanonicalMirror: read view and canonical mirror health
  Syncer->>StateJSON: publish listener heartbeat and connection health
  WorkspaceCLI->>StateJSON: read persisted listener health
  WorkspaceCLI-->>WorkspaceCLI: report view and listener status
Loading

Possibly related PRs

Suggested reviewers: khaliqgant

Poem

I’m a rabbit with views in a row,
Through symlinked paths, the records flow.
The listener beats, its health made clear,
Heartbeats and retries now appear.
The mirror stays safe as I hop near.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: projecting one workspace mirror into multiple local views.
Description check ✅ Passed The description accurately explains workspace views, canonical mirror ownership, health reporting, listener liveness, and verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/409-multi-view-mirror
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/409-multi-view-mirror

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (6)
cmd/relayfile-cli/main.go (2)

5637-5647: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The symlink survives a failed record write.

installWorkspaceView creates the symlink first. If upsertWorkspaceDetails then fails, the symlink stays on disk but no view record exists. relayfile workspace view remove cannot remove it, because workspaceViewByLocalDir finds no match. Remove the newly created symlink when persistence fails.

♻️ Proposed refactor
 	if err := installWorkspaceView(localDir, target, *replace); err != nil {
 		return err
 	}
 	record.Views = upsertWorkspaceView(record.Views, workspaceViewRecord{
 		RemotePath: remotePath,
 		LocalDir:   localDir,
 		CreatedAt:  time.Now().UTC().Format(time.RFC3339),
 	})
 	if _, err := upsertWorkspaceDetails(record); err != nil {
+		_ = removeWorkspaceViewLink(localDir, target)
 		return err
 	}
🤖 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 `@cmd/relayfile-cli/main.go` around lines 5637 - 5647, Update the
workspace-view creation flow around installWorkspaceView and
upsertWorkspaceDetails so that, when persistence fails after the symlink is
installed, the newly created symlink is removed before returning the error.
Limit cleanup to the newly created view and preserve the existing error
propagation.

5670-5676: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

view list reports nothing when the mirror is not configured, and emits null in JSON.

buildWorkspaceHealthReport returns early when record.LocalDir is empty, so report.Views is nil even when views are registered. With --json, writeJSON then writes null instead of an empty array. Build the view list from record.Views directly, and normalize a nil slice to []workspaceViewHealth{} before encoding.

🤖 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 `@cmd/relayfile-cli/main.go` around lines 5670 - 5676, Update the view-listing
flow around buildWorkspaceHealthReport to derive views directly from
record.Views rather than relying on report.Views when record.LocalDir is empty.
Before passing the result to writeJSON, normalize a nil slice to an empty
[]workspaceViewHealth so JSON output is [] and preserve the existing text
formatting.
internal/mountsync/syncer.go (2)

7353-7368: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify what listenerHeartbeatAt measures.

markSyncSuccess and markSyncError update listenerHeartbeatAt on every reconcile result, including errors. The value therefore tracks reconcile-loop liveness, not websocket listener liveness. canonicalViewStatus in cmd/relayfile-cli/main.go uses this timestamp to decide stale, so a websocket that is disconnected but still polled will keep a fresh heartbeat. The separate Status field still exposes the disconnect, so this is not a correctness defect. Add a comment on the field to record the intended meaning.

🤖 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 7353 - 7368, Add a comment to the
listenerHeartbeatAt field clarifying that it records reconcile-loop liveness,
updated on both successful and failed reconcile results, rather than websocket
listener connectivity. Mention that canonicalViewStatus uses it for staleness
checks, while websocket disconnect state is represented separately by Status.

3500-3507: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a distinct status before the first connection attempt.

If websocket is enabled and no dial has started, wsConn is nil and wsConnecting is false. The status is then reconnecting, but no connection was ever attempted. Consumers cannot tell "never connected" from "connection lost" without also reading LastConnectedAt.

♻️ Optional refinement
 	health.Mode = "websocket"
 	switch {
 	case s.wsConn != nil:
 		health.Status = "listening"
 	case s.wsConnecting:
 		health.Status = "connecting"
+	case s.wsLastAttemptAt.IsZero():
+		health.Status = "starting"
 	default:
 		health.Status = "reconnecting"
 	}
🤖 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 3500 - 3507, Update the health
status selection around wsConn and wsConnecting to distinguish the initial
websocket-enabled state before any dial has started from the reconnecting state
after a failed or lost connection. Use the existing connection-attempt or
connection-history indicator to identify whether a connection was ever
attempted, assign a distinct “never connected” status for the initial state, and
preserve “listening” and “connecting” for their current conditions.
cmd/relayfile-cli/main_test.go (2)

6959-7016: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test depends on the default mirror remote root.

The test never writes .relay/state.json into canonical, yet workspace view add must succeed. workspaceViewTarget calls readMountRemoteRoot(canonical) and then normalizeWorkspaceViewRemotePath, which rejects an empty string. The test therefore asserts, implicitly, that readMountRemoteRoot returns a non-empty default. Make that dependency explicit, or add a case that writes an explicit remoteRoot.

Note also that this test creates a symlink. On Windows, symlink creation requires elevated privileges or developer mode. Add a skip guard if the suite runs on Windows.

🤖 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 `@cmd/relayfile-cli/main_test.go` around lines 6959 - 7016, Update
TestWorkspaceViewsShareCanonicalMirrorWithoutRehome to make the mirror
remote-root setup explicit by writing the expected .relay/state.json
configuration into canonical, or add a separate explicit-remoteRoot case while
preserving the current behavior. Also add a Windows skip guard before symlink
creation so the test does not fail where symlinks are unavailable.

7045-7066: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case where eventListener is absent.

This test always writes an eventListener block. canonicalViewStatus returns not-listening when state.EventListener is nil, which is the state file produced by an older daemon. Add a case that omits EventListener and asserts the expected view status. This locks the backward-compatibility behavior discussed on cmd/relayfile-cli/main.go Line 5996.

🤖 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 `@cmd/relayfile-cli/main_test.go` around lines 7045 - 7066, The test around
writeMirrorStateFile and buildWorkspaceHealthReport only covers a present
EventListener; add a separate state-file case that omits EventListener entirely,
then assert the resulting view status is “not-listening” while preserving the
existing listener-state assertions for the current case.
🤖 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 `@cmd/relayfile-cli/main.go`:
- Around line 5996-6016: Update canonicalViewStatus in
cmd/relayfile-cli/main.go:5996-6016 to remove the nil EventListener else branch,
allowing absent listener state to fall through and return the trimmed
state.Status. Add a cmd/relayfile-cli/main_test.go:7045-7066 case with no
EventListener block that asserts the view status follows state.Status.
- Line 5732: Update the backslash normalization in the surrounding
value-processing logic to replace individual backslash characters with “/”,
using the correct Go string literal rather than the current two-character
pattern. Preserve all other value transformations unchanged.

---

Nitpick comments:
In `@cmd/relayfile-cli/main_test.go`:
- Around line 6959-7016: Update
TestWorkspaceViewsShareCanonicalMirrorWithoutRehome to make the mirror
remote-root setup explicit by writing the expected .relay/state.json
configuration into canonical, or add a separate explicit-remoteRoot case while
preserving the current behavior. Also add a Windows skip guard before symlink
creation so the test does not fail where symlinks are unavailable.
- Around line 7045-7066: The test around writeMirrorStateFile and
buildWorkspaceHealthReport only covers a present EventListener; add a separate
state-file case that omits EventListener entirely, then assert the resulting
view status is “not-listening” while preserving the existing listener-state
assertions for the current case.

In `@cmd/relayfile-cli/main.go`:
- Around line 5637-5647: Update the workspace-view creation flow around
installWorkspaceView and upsertWorkspaceDetails so that, when persistence fails
after the symlink is installed, the newly created symlink is removed before
returning the error. Limit cleanup to the newly created view and preserve the
existing error propagation.
- Around line 5670-5676: Update the view-listing flow around
buildWorkspaceHealthReport to derive views directly from record.Views rather
than relying on report.Views when record.LocalDir is empty. Before passing the
result to writeJSON, normalize a nil slice to an empty []workspaceViewHealth so
JSON output is [] and preserve the existing text formatting.

In `@internal/mountsync/syncer.go`:
- Around line 7353-7368: Add a comment to the listenerHeartbeatAt field
clarifying that it records reconcile-loop liveness, updated on both successful
and failed reconcile results, rather than websocket listener connectivity.
Mention that canonicalViewStatus uses it for staleness checks, while websocket
disconnect state is represented separately by Status.
- Around line 3500-3507: Update the health status selection around wsConn and
wsConnecting to distinguish the initial websocket-enabled state before any dial
has started from the reconnecting state after a failed or lost connection. Use
the existing connection-attempt or connection-history indicator to identify
whether a connection was ever attempted, assign a distinct “never connected”
status for the initial state, and preserve “listening” and “connecting” for
their current conditions.
🪄 Autofix

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: 09b853eb-7ae5-4f7a-9c7b-a7dba17cf08e

📥 Commits

Reviewing files that changed from the base of the PR and between a4e8505 and fe8eae1.

📒 Files selected for processing (3)
  • cmd/relayfile-cli/main.go
  • cmd/relayfile-cli/main_test.go
  • internal/mountsync/syncer.go

Comment thread cmd/relayfile-cli/main.go Outdated
Comment thread cmd/relayfile-cli/main.go

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cmd/relayfile-cli/main.go
Comment thread internal/mountsync/syncer.go Outdated
Comment thread internal/mountsync/syncer.go
Comment thread cmd/relayfile-cli/main.go
Comment thread cmd/relayfile-cli/main.go Outdated
Comment thread cmd/relayfile-cli/main.go
Comment thread cmd/relayfile-cli/main.go Outdated
@kjgbot
kjgbot force-pushed the fix/409-multi-view-mirror branch from fe8eae1 to 4b08411 Compare August 8, 2026 20:20
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-08-08T20-22-28-627Z-HEAD-provider
Mode: provider
Git SHA: 635b21f

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.

@khaliqgant
khaliqgant merged commit 6b06a4c into main Aug 8, 2026
10 checks passed
@khaliqgant
khaliqgant deleted the fix/409-multi-view-mirror branch August 8, 2026 20:59
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.

[factory] A workspace can be mirrored in only one directory, so two local consumers cannot both have a live view

2 participants