Skip to content

fix: share one Relayfile workspace mirror across routed repos - #220

Merged
khaliqgant merged 8 commits into
mainfrom
fix/issue-218-219-workspace-mount
Aug 8, 2026
Merged

fix: share one Relayfile workspace mirror across routed repos#220
khaliqgant merged 8 commits into
mainfrom
fix/issue-218-219-workspace-mount

Conversation

@miyaontherelay

@miyaontherelay miyaontherelay commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #218
Fixes #219

Root cause confirmed

Factory derived a Relayfile mirror from every routed checkout, although Relayfile admits one local directory per workspace. That made the first mount succeed and every later route request a rejected re-home. The stale-repair path repeated that same checkout-derived target, so it could not heal the registered mirror. The diagnosis matches the live incident.

Change

  • Resolve a workspace-scoped mirror once from an explicit localMountRoot, Relayfile's local registration, or (for older installs) the authoritative "already mirrored at ..." admission response. The fallback retries only that registered root; it never passes --rehome.
  • Preflight one mirror per workspace, pass its absolute path to spawned agents and standalone babysitters, and report a routed-repo/mount summary.
  • Make factory status expose localMountDegraded, its reason, and the mirror root.
  • Treat a mirror as stale after 3 Relayfile poll intervals: 3 x 30 seconds = 90 seconds. Three intervals permits one missed poll plus filesystem jitter, but surfaces a stopped mirror in 90 seconds rather than hours.

Local proof (built Factory 0.1.57; private scratch workspace and mirror only)

The scratch daemon used the real rebuilt Relayfile client/mirror and 16 configured routed repos. Its empty scratch workspace has no GitHub integration, so the daemon harness bypassed only that unrelated readiness gate; the mount and Factory start path were otherwise production code.

[factory] refreshed 1 stale local mount(s) (last reconcile ~5m ago)
[factory] Relayfile workspace mirror preflight: mounted=1 failed=0 routedRepos=16

A direct 16-route run through the same built mount client recorded one SDK mount launch and one target:

[factory] local mount at /private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations is stale (last reconcile 5m ago); refreshing
[factory] local mount at /private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations refreshed
{"routedRepos":16,"successfulRoutes":16,"failedRoutes":0,"uniqueMirrorTargets":["/private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations"],"sdkMountStartsDuringRun":1}

Red-check: after backdating that scratch mirror by five minutes, the built CLI surfaced degradation:

$ factory status --config /private/tmp/factory-mount-proof-218-219.gWR5jL/factory.config.json
{
  "slackDegraded": false,
  "localMountDegraded": true,
  "localMountDegradedReason": "last reconcile 5m ago",
  "localMountRoot": "/private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations"
}

Red-check/heal: the stale refresh was invoked with an unrelated routed checkout, and the output shows it targeted the registered scratch root and succeeded:

[factory] local mount at /private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations is stale (last reconcile 6m ago); refreshing
[factory] local mount at /private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations refreshed
{"refresh":"succeeded","target":"/private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations","health":{"degraded":false,"localDir":"/private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations"}}

Verification:

tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json
vitest run src/mount/local-mount-preflight.test.ts src/mount/workspace-mirror.test.ts src/mount/relayfile-binary.test.ts src/mount/relayfile-cloud-mount-client.test.ts src/cli/fleet.test.ts
# 5 files passed, 166 tests passed
node bin/factory.mjs --version
# 0.1.57

@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: 58 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: e71aa86a-7a41-4081-b56b-568782479bb5

📥 Commits

Reviewing files that changed from the base of the PR and between 1ce4d21 and 9f39c85.

📒 Files selected for processing (15)
  • src/cli/fleet.test.ts
  • src/cli/fleet.ts
  • src/config/schema.test.ts
  • src/config/schema.ts
  • src/mount/local-mount-preflight.test.ts
  • src/mount/local-mount-preflight.ts
  • src/mount/relayfile-binary.test.ts
  • src/mount/relayfile-binary.ts
  • src/mount/relayfile-cloud-mount-client.test.ts
  • src/mount/relayfile-cloud-mount-client.ts
  • src/mount/workspace-mirror.test.ts
  • src/mount/workspace-mirror.ts
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/types.ts
📝 Walkthrough

Walkthrough

Factory now mounts one registered workspace mirror for all routed repositories. It resolves explicit or persisted mirror roots, reuses the root across mount operations, reports local mount health, refreshes stale mirrors, and updates CLI, babysitter, and orchestration paths.

Changes

Shared workspace mirror

Layer / File(s) Summary
Mirror contracts and resolution
src/config/schema.ts, src/ports/*, src/mount/workspace-mirror.*
Workspace configuration accepts localMountRoot. Public mount contracts expose local root and health. Mirror resolution checks workspace registration, then valid mount-state records.
Mirror-aware mount client
src/mount/relayfile-cloud-mount-client.*
RelayfileCloudMountClient resolves and reuses one mirror across routes, retries authoritative registered paths, and reports degraded or stale health. Tests cover registration, fallback, recovery, and single-operation concurrency.
Factory workspace mount flow
src/cli/fleet.*, src/orchestrator/factory.ts
Factory startup, run-once, loop, and babysitting use the workspace mount. Status includes mount health and root. Integration paths use the resolved mirror. Tests cover asynchronous warming and sixteen routes.
Staleness threshold and refresh logging
src/mount/relayfile-binary.*, src/mount/local-mount-preflight.*
Staleness derives from exported sync constants. Refresh messages identify the registered local mount path. Tests cover the threshold and log output.

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

Possibly related PRs

Suggested reviewers: khaliqgant, kjgbot

Poem

I’m a rabbit with one mirror bright,
Mounting shared paths just right.
Stale roots now call their name,
Health reports reveal the flame.
Sixteen routes warm as one—
Hop, Factory, the work is done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy #218 and #219 by sharing one workspace mirror, resolving registered refresh targets, and exposing local-mount degradation.
Out of Scope Changes check ✅ Passed The code and test changes support the linked objectives, including configuration, mount health, stale detection, CLI behavior, and integration paths.
Title check ✅ Passed The title clearly and concisely describes the main change: sharing one Relayfile workspace mirror across routed repositories.
Description check ✅ Passed The description directly explains the root cause, implementation, objectives, and verification for the workspace mirror changes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-218-219-workspace-mount

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: 4

🤖 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 `@src/config/schema.ts`:
- Around line 237-240: Update the localMountRoot schema validation to trim
input, reject blank values, and require an absolute path before the value
reaches the mount client. Preserve the optional behavior when the setting is
omitted, and ensure the validated value is the trimmed absolute path.

In `@src/mount/relayfile-cloud-mount-client.ts`:
- Around line 391-410: Move the authoritative registered-root retry from the
per-caller flow around _localMountRoot and _runLocalMountOperation into the
shared mount operation so concurrent unresolved callers await the same
fallback-and-retry sequence. Ensure the operation retries only when the
admission error provides a different registered root, while preserving
explicit/configured roots and existing cleanup behavior. Add a concurrent test
covering two unresolved callers receiving the same admission error and both
completing through the registered-root retry.
- Around line 331-333: Update the local mount root resolution in the constructor
to query both this.workspaceId and the Relayfile workspace UUID, matching the
identifier set used by the resolution path around lines 368-370. Apply the same
two-identifier input to config.workspaceMirrorResolver and
resolveRegisteredWorkspaceMirror while preserving the existing precedence and
fallback behavior.

In `@src/mount/workspace-mirror.ts`:
- Around line 40-45: Update the workspace registry lookup loop to collect
distinct normalized local roots for all accepted matching records instead of
returning the first result. Return the resolved root only when exactly one root
matches; otherwise return undefined, consistent with readMountStateDirectory().
Add coverage for two matching records with different roots.
🪄 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: b83909ad-3a48-415c-a311-371f60673d79

📥 Commits

Reviewing files that changed from the base of the PR and between 16ad97e and 1ce4d21.

📒 Files selected for processing (14)
  • src/cli/fleet.test.ts
  • src/cli/fleet.ts
  • src/config/schema.ts
  • src/mount/local-mount-preflight.test.ts
  • src/mount/local-mount-preflight.ts
  • src/mount/relayfile-binary.test.ts
  • src/mount/relayfile-binary.ts
  • src/mount/relayfile-cloud-mount-client.test.ts
  • src/mount/relayfile-cloud-mount-client.ts
  • src/mount/workspace-mirror.test.ts
  • src/mount/workspace-mirror.ts
  • src/orchestrator/factory.ts
  • src/ports/index.ts
  • src/ports/mount.ts

Comment thread src/config/schema.ts Outdated
Comment thread src/mount/relayfile-cloud-mount-client.ts Outdated
Comment thread src/mount/relayfile-cloud-mount-client.ts Outdated
Comment thread src/mount/workspace-mirror.ts
@miyaontherelay

Copy link
Copy Markdown
Contributor Author

Follow-up after CodeRabbit findings: implemented all four valid fixes in 5f5cb2b (absolute trimmed localMountRoot validation; both workspace aliases; shared concurrent admission fallback; ambiguous registry roots rejected).\n\nFinal verification:\n- tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json\n- 6 focused test files passed, 195 tests passed\n- final rebuilt scratch daemon preflight: mounted=1 failed=0 routedRepos=16\n- final real 16-route run: successfulRoutes=16 failedRoutes=0 uniqueMirrorTargets=1 sdkMountStartsDuringRun=1\n- final stale status red-check: localMountDegraded=true, reason last reconcile 5m ago.\n\nNo re-home was requested.

@miyaontherelay

Copy link
Copy Markdown
Contributor Author

Follow-up for the event-transport requirement (commit 718ef84b976aa80590831862f406791161549312):

factory status now returns two independent signals:

  • eventListener: persisted in the live daemon heartbeat after Factory has registered its Relayfile subscription or poller; missing/stale/stopping heartbeat is explicitly not-listening.
  • localMountEventFeed: healthy or degraded, derived from the registered mirror’s .integrations/.relay/state.json.

That distinguishes a quiet but registered feed from no listener, and a registered listener with a stale/dead mount feed. Listener status is registration state, not a claim that a websocket has delivered a recent event.

Actual output from the rebuilt 0.1.57 binary against the private scratch mirror after the red-check backdate:

$ node bin/factory.mjs status --config /private/tmp/factory-mount-proof-218-219.gWR5jL/factory.config.json
[factory] reusing the relay broker that is already running
{
  "inFlight": [],
  "queued": [],
  "parked": [],
  "counters": {},
  "slackDegraded": false,
  "eventListener": {
    "state": "not-listening",
    "reason": "heartbeat stale"
  },
  "localMountDegraded": true,
  "localMountDegradedReason": "last reconcile 2m ago",
  "localMountRoot": "/private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations",
  "localMountEventFeed": {
    "state": "degraded",
    "livenessSignal": ".integrations/.relay/state.json",
    "reason": "last reconcile 2m ago",
    "root": "/private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations"
  }
}

Focused verification after the update:

tsc -p tsconfig.build.json --noEmit
vitest run src/cli/fleet.test.ts src/orchestrator/factory.test.ts --reporter=dot
# 2 files passed

The scratch workspace has no GitHub integration, so this patch does not claim an end-to-end provider webhook delivery test; it proves mount/liveness and listener-registration reporting.

@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

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

Re-trigger cubic

Comment thread src/mount/relayfile-binary.ts
@miyaontherelay

Copy link
Copy Markdown
Contributor Author

Review follow-up (valid non-default-cadence finding) in b6f054fa39d7052e0d0f4906139a2ea6b03ff737:

The stale threshold is now three intervals from the registered mirror’s .integrations/.relay/state.json intervalMs. The 30-second Relayfile default is used only if that value is absent, non-finite, or below one second; therefore normal mirrors remain 90 seconds, while a registered 2-minute mirror is not falsely refreshed after 90 seconds. Three intervals still permits one missed reconcile plus filesystem jitter before calling the event feed degraded.

tsc -p tsconfig.build.json --noEmit
vitest run src/mount/relayfile-binary.test.ts src/mount/local-mount-preflight.test.ts src/mount/relayfile-cloud-mount-client.test.ts src/cli/fleet.test.ts --reporter=dot
# 4 files passed, 167 tests passed
tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json
node bin/factory.mjs --version
# 0.1.57

The added regression puts a live 2-minute mount beyond the default 90-second cutoff and verifies it remains healthy; another verifies invalid intervalMs falls back to the default.

@miyaontherelay

Copy link
Copy Markdown
Contributor Author

CI on b6f054f caught a status-reporting test I added: an explicit transport: "poll" override was not retained, so the daemon heartbeat reported starting. Fixed in f1790d4f9204f3957bc17973a74b97dacf32a028 by persisting the effective transport selected for that start (including overrides). Both subscription and poll listener regressions pass after rebuilding 0.1.57; replacement CI is run 31202986407.

@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

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

Re-trigger cubic

Comment thread src/cli/fleet.ts
Comment thread src/mount/relayfile-cloud-mount-client.ts Outdated
Comment thread src/mount/relayfile-cloud-mount-client.ts
Comment thread src/mount/workspace-mirror.ts Outdated
Comment thread src/mount/relayfile-cloud-mount-client.ts
@miyaontherelay

Copy link
Copy Markdown
Contributor Author

Post-review hardening in 0162008be162e2a2153d72c6622fed265c6d280b:

  • an unknown mirror must resolve before factory start enables live dispatch; a failed resolution now aborts startup instead of giving agents the provisional checkout path;
  • exact registered roots work even when not named .integrations;
  • legacy relative roots are anchored to Relayfile home;
  • the admission fallback accepts platform-native absolute paths;
  • fromConfig performs the registration lookup exactly once.

All five Cubic review threads were answered and resolved. Final focused verification after rebuilding:

tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json
vitest run src/mount/workspace-mirror.test.ts src/mount/local-mount-preflight.test.ts src/mount/relayfile-cloud-mount-client.test.ts src/cli/fleet.test.ts --reporter=dot
# 4 files passed, 164 tests passed
node bin/factory.mjs --version
# 0.1.57

Actual rebuilt-binary scratch red-check remains explicit after this commit:

$ node bin/factory.mjs status --config /private/tmp/factory-mount-proof-218-219.gWR5jL/factory.config.json
...
"eventListener": { "state": "not-listening", "reason": "heartbeat stale" },
"localMountDegraded": true,
"localMountEventFeed": {
  "state": "degraded",
  "livenessSignal": ".integrations/.relay/state.json",
  "reason": "last reconcile 1m ago",
  "root": "/private/tmp/factory-mount-proof-218-219.gWR5jL/chief/.integrations"
}

The scratch mount is still the only mount touched; no re-home was requested.

@miyaontherelay

Copy link
Copy Markdown
Contributor Author

Final remote verification for 0162008be162e2a2153d72c6622fed265c6d280b: CI run 31203736181 completed successfully on that exact SHA. Every workflow passed: package, load-e2e, verification-stack-e2e, kubernetes-provider-e2e, and verification-gate-e2e. The PR remains open and unmerged.

@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 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/cli/fleet.ts Outdated
@khaliqgant
khaliqgant merged commit 33cda42 into main Aug 8, 2026
7 checks passed
@khaliqgant
khaliqgant deleted the fix/issue-218-219-workspace-mount branch August 8, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants