Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/cli/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3468,6 +3468,7 @@ describe('fleet CLI runtime', () => {
const mount = Object.assign(new FakeMountClient(), {
getLocalMountRoot: () => mirrorDir,
})
const errors = buffer()

await runFleetCli(['start', '--config', configPath], {
fleet: new FakeFleetClient(),
Expand All @@ -3479,13 +3480,15 @@ describe('fleet CLI runtime', () => {
await vi.waitFor(() => expect(mounted).toHaveLength(1))
}),
stdout: buffer(),
stderr: buffer(),
stderr: errors,
})

expect(mounted).toEqual([dirname(mirrorDir)])
expect(ensureLocalMount).toHaveBeenCalledTimes(1)
expect(mountedWhenFactoryStarted).toBeLessThan(1)
expect(factory.start).toHaveBeenCalledWith({ mode: 'live' })
expect(errors.text()).not.toContain('could not start relayfile mount')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The assertion expect(errors.text()).not.toContain('could not start relayfile mount') can never fail, because the code emits no such string. The actual stderr warnings are [factory] warning: could not start Relayfile workspace mirror at ... (fleet.ts:1041) and [factory] warning: background relayfile mount warmup failed: ... (fleet.ts:684). The meaningful check is the following assertion on could not start Relayfile workspace mirror; drop this dead assertion or point it at a string the code actually produces so it guards real output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/fleet.test.ts, line 3490:

<comment>The assertion `expect(errors.text()).not.toContain('could not start relayfile mount')` can never fail, because the code emits no such string. The actual stderr warnings are `[factory] warning: could not start Relayfile workspace mirror at ...` (fleet.ts:1041) and `[factory] warning: background relayfile mount warmup failed: ...` (fleet.ts:684). The meaningful check is the following assertion on `could not start Relayfile workspace mirror`; drop this dead assertion or point it at a string the code actually produces so it guards real output.</comment>

<file context>
@@ -3479,13 +3480,15 @@ describe('fleet CLI runtime', () => {
       expect(ensureLocalMount).toHaveBeenCalledTimes(1)
       expect(mountedWhenFactoryStarted).toBeLessThan(1)
       expect(factory.start).toHaveBeenCalledWith({ mode: 'live' })
+      expect(errors.text()).not.toContain('could not start relayfile mount')
+      expect(errors.text()).not.toContain('could not start Relayfile workspace mirror')
     } finally {
</file context>

expect(errors.text()).not.toContain('could not start Relayfile workspace mirror')
} finally {
await rm(root, { recursive: true, force: true })
}
Expand Down
60 changes: 59 additions & 1 deletion src/mount/relayfile-binary.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { dirname, join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, describe, expect, it, vi } from 'vitest'

import {
checkMountStaleness,
isMountProcessRunning,
RELAYFILE_SYNC_INTERVAL_MS,
STALE_RECONCILE_MS,
STALE_RECONCILE_INTERVALS,
Expand Down Expand Up @@ -39,6 +40,13 @@ async function writeState(
return statePath
}

async function writePidState(
dir: string,
state: { pid: number; workspaceId?: string; localDir?: string } | number,
): Promise<void> {
await writeFile(join(dir, 'mount.pid'), JSON.stringify(state), 'utf8')
}

describe('checkMountStaleness', () => {
it('leaves missing state non-stale so the caller can decide whether to start', async () => {
await withTempDir(async (dir) => {
Expand Down Expand Up @@ -195,6 +203,38 @@ describe('checkMountStaleness', () => {
})
})

it('uses the registered mount.pid when public state omits process ownership', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
workspaceId: 'rw_test',
lastReconcileAt: new Date().toISOString(),
})
await writePidState(dir, {
pid: process.pid,
workspaceId: 'rw_test',
localDir: dirname(dir),
})

expect(checkMountStaleness(statePath, 'rw_test')).toEqual({ stale: false, pid: process.pid })
})
})

it('ignores a mount.pid registered for a different workspace', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
workspaceId: 'rw_test',
lastReconcileAt: new Date().toISOString(),
})
await writePidState(dir, {
pid: 12345,
workspaceId: 'rw_other',
localDir: dirname(dir),
})

expect(checkMountStaleness(statePath, 'rw_test')).toEqual({ stale: false })
})
})

it('marks a stale mount via a dead daemon.pid even within the reconcile window', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
Expand Down Expand Up @@ -239,3 +279,21 @@ describe('checkMountStaleness', () => {
})
})
})

describe('isMountProcessRunning', () => {
it('treats an EPERM probe as a running process owned by another user', () => {
vi.spyOn(process, 'kill').mockImplementation(() => {
throw Object.assign(new Error('not permitted'), { code: 'EPERM' })
})

expect(isMountProcessRunning(12345)).toBe(true)
})

it('rejects a missing or dead process', () => {
expect(isMountProcessRunning(undefined)).toBe(false)
vi.spyOn(process, 'kill').mockImplementation(() => {
throw Object.assign(new Error('not found'), { code: 'ESRCH' })
})
expect(isMountProcessRunning(12345)).toBe(false)
})
})
56 changes: 49 additions & 7 deletions src/mount/relayfile-binary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { readFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'

// Relayfile's poll mirror reconciles every 30 seconds by default. Three
// intervals allow one missed poll and ordinary filesystem jitter, while still
Expand Down Expand Up @@ -30,10 +31,54 @@ type MountState = {
daemon?: { pid?: unknown }
}

type MountPidState = {
pid?: unknown
workspaceId?: unknown
localDir?: unknown
}

export function coercePid(value: unknown): number | undefined {
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined
}

/** Read-only liveness probe used to distinguish an attached daemon from a mount Factory owns. */
export function isMountProcessRunning(pid: number | undefined): boolean {
if (pid === undefined) return false
try {
process.kill(pid, 0)
return true
} catch (error) {
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'EPERM'
}
}

function readMountProcessPid(
stateFilePath: string,
acceptedWorkspaceIds: ReadonlySet<string>,
): number | undefined {
let raw: string
try {
raw = readFileSync(join(dirname(stateFilePath), 'mount.pid'), 'utf8')
} catch {
return undefined
}

try {
const parsed = JSON.parse(raw) as MountPidState | number
if (typeof parsed === 'number') return coercePid(parsed)
if (typeof parsed !== 'object' || parsed === null) return undefined
const workspaceId = typeof parsed.workspaceId === 'string' ? parsed.workspaceId : undefined
if (workspaceId && !acceptedWorkspaceIds.has(workspaceId)) return undefined
const localDir = typeof parsed.localDir === 'string' ? parsed.localDir : undefined
const expectedLocalDir = dirname(dirname(stateFilePath))
if (localDir && resolve(localDir) !== resolve(expectedLocalDir)) return undefined
return coercePid(parsed.pid)
} catch {
const legacyPid = Number(raw.trim())
return coercePid(legacyPid)
}
}

export function checkMountStaleness(
stateFilePath: string,
workspaceId: string,
Expand Down Expand Up @@ -67,7 +112,9 @@ export function checkMountStaleness(
}

// Prefer the top-level pid; fall back to the SDK-launched mount's daemon.pid.
const pid = coercePid(parsed.pid) ?? coercePid(parsed.daemon?.pid)
const pid = coercePid(parsed.pid) ??
coercePid(parsed.daemon?.pid) ??
readMountProcessPid(stateFilePath, accepted)

const lastReconcileAt = typeof parsed.lastReconcileAt === 'string'
? Date.parse(parsed.lastReconcileAt)
Expand All @@ -94,12 +141,7 @@ export function checkMountStaleness(
return { stale: false }
}

try {
process.kill(pid, 0)
} catch (error) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EPERM') {
return { stale: false, pid }
}
if (!isMountProcessRunning(pid)) {
return { stale: true, reason: `mount process (pid ${pid}) is not running`, pid }
}

Expand Down
72 changes: 72 additions & 0 deletions src/mount/relayfile-cloud-mount-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,78 @@ describe('RelayfileCloudMountClient', () => {
}
})

it('attaches to an externally supervised registered mirror without replacing it when health turns stale', async () => {
vi.useFakeTimers()
vi.setSystemTime('2026-08-14T12:00:00.000Z')
const root = await mkdtemp(join(tmpdir(), 'factory-external-workspace-mirror-'))
const localDir = join(root, 'chief', '.integrations')
const stateDir = join(localDir, '.relay')
await mkdir(stateDir, { recursive: true })
const writeState = async (lastReconcileAt: string): Promise<void> => {
await writeFile(join(stateDir, 'state.json'), JSON.stringify({
workspaceId: 'cloud-workspace-uuid',
lastReconcileAt,
intervalMs: 1_000,
}))
}
await writeState('2026-08-14T12:00:00.000Z')
await writeFile(join(stateDir, 'mount.pid'), JSON.stringify({
pid: process.pid,
workspaceId: 'cloud-workspace-uuid',
localDir,
}))

const fake = new FakeRelayFileClient()
const ensureMountedWorkspace = vi.fn(async () => ({ stop: vi.fn(async () => {}) }))
const healthEvents: Array<{ state: string; reason: string; degradedMounts: number }> = []
const mount = new RelayfileCloudMountClient({
workspaceId: 'rw_shared',
client: fake,
relayfileSetup: { joinWorkspace: vi.fn(), ensureMountedWorkspace },
relayfileWorkspace: {
workspaceId: 'cloud-workspace-uuid',
client: () => fake,
getToken: async () => 'delegated-relayfile-token',
info: { relayfileUrl: 'https://relayfile.example' },
},
localMountRoot: localDir,
localMountHealthIntervalMs: 1_000,
onLocalMountHealth: (event) => { healthEvents.push(event) },
})

try {
await mount.ensureLocalMount(join(root, 'unrelated-repository'))
expect(ensureMountedWorkspace).not.toHaveBeenCalled()

// The daemon is still alive but has missed its reconcile threshold.
// Factory must report that degradation without launching a replacement.
await writeState('2026-08-14T11:59:50.000Z')
await vi.advanceTimersByTimeAsync(1_000)
expect(ensureMountedWorkspace).not.toHaveBeenCalled()
expect(healthEvents).toEqual([{
state: 'degraded',
reason: 'mount_stale',
degradedMounts: 1,
}])

// Recovery is also observed in place; the external process remains the
// only daemon serving the mirror throughout the client lifetime.
await writeState('2026-08-14T12:00:02.000Z')
await vi.advanceTimersByTimeAsync(1_000)
expect(ensureMountedWorkspace).not.toHaveBeenCalled()
expect(healthEvents).toEqual([
{ state: 'degraded', reason: 'mount_stale', degradedMounts: 1 },
{ state: 'recovered', reason: 'mount_stale', degradedMounts: 0 },
])
} finally {
await mount.dispose()
await rm(root, { recursive: true, force: true })
vi.useRealTimers()
}

expect(ensureMountedWorkspace).not.toHaveBeenCalled()
})

it('resolves a direct client mirror through the cloud workspace identifier alias', async () => {
const fake = new FakeRelayFileClient()
const resolver = vi.fn((workspaceIds: readonly string[]) =>
Expand Down
36 changes: 35 additions & 1 deletion src/mount/relayfile-cloud-mount-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
ensureLocalMount as runLocalMountPreflight,
type EnsureLocalMountOptions,
} from './local-mount-preflight'
import { checkMountStaleness } from './relayfile-binary'
import { checkMountStaleness, isMountProcessRunning } from './relayfile-binary'
import { MountAuthScopeError } from './mount-auth-error'
import { resolveRegisteredWorkspaceMirror } from './workspace-mirror'

Expand Down Expand Up @@ -279,6 +279,11 @@ export class RelayfileCloudMountClient implements MountClient {
readonly #localMountScopes: string[]
#localMountRoot?: string
readonly #localMounts = new Map<string, MountedWorkspaceHandleLike>()
// A registered mirror that was already being served when Factory attached
// belongs to another supervisor. Once observed, keep that ownership boundary
// for this client lifetime: Factory may monitor it but must never replace or
// stop it, even if a later reconcile is stale while the daemon recovers.
readonly #externallyManagedLocalMounts = new Set<string>()
readonly #localMountSupervisions = new Map<string, {
startDir: string
options: LocalMountOptions
Expand Down Expand Up @@ -464,11 +469,22 @@ export class RelayfileCloudMountClient implements MountClient {
const staleBefore = existsSync(statePath)
? checkMountStaleness(statePath, this.workspaceId, [...acceptableWorkspaceIds])
: undefined
Comment on lines 469 to 471

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Probe mount.pid before starting an uninitialized mirror

When Factory and an external launchd/systemd mount start concurrently, the daemon can have written .relay/mount.pid but still be waiting for its first reconcile to create state.json; this code skips the PID probe whenever the state file is absent. ensureLocalMount then follows the missing-state branch in src/mount/local-mount-preflight.ts and invokes startMount(), launching a competing SDK daemon in the externally owned directory. Probe and validate mount.pid independently of state.json before allowing the bootstrap path.

Useful? React with 👍 / 👎.

if (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When state.json is absent because the externally supervised daemon just started and hasn't completed its first reconcile yet, staleBefore is undefined and the mirror is never added to #externallyManagedLocalMounts. This causes the missing-state branch to invoke startMount(), launching a competing SDK daemon in a directory already owned by an external supervisor. Probe .relay/mount.pid directly (independent of state.json) before falling back to the startMount path so an already-registered external daemon is recognized even before its first reconcile.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mount/relayfile-cloud-mount-client.ts, line 472:

<comment>When state.json is absent because the externally supervised daemon just started and hasn't completed its first reconcile yet, `staleBefore` is `undefined` and the mirror is never added to `#externallyManagedLocalMounts`. This causes the missing-state branch to invoke `startMount()`, launching a competing SDK daemon in a directory already owned by an external supervisor. Probe `.relay/mount.pid` directly (independent of `state.json`) before falling back to the startMount path so an already-registered external daemon is recognized even before its first reconcile.</comment>

<file context>
@@ -464,11 +469,22 @@ export class RelayfileCloudMountClient implements MountClient {
     const staleBefore = existsSync(statePath)
       ? checkMountStaleness(statePath, this.workspaceId, [...acceptableWorkspaceIds])
       : undefined
+    if (
+      !this.#localMounts.has(localDir) &&
+      staleBefore !== undefined &&
</file context>

!this.#localMounts.has(localDir) &&
staleBefore !== undefined &&
(!staleBefore.stale || isMountProcessRunning(staleBefore.pid))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The ownership probe treats any pre-existing healthy state (or a stale state whose pid is still alive) as externally managed, regardless of who launched the daemon. When Factory restarts, its own previously-spawned detached daemon (created via ensureMountedWorkspace with background: true) still writes a fresh state.json and mount.pid, and #localMounts is empty on the new process — so on the first #ensureLocalMount this branch adds it to #externallyManagedLocalMounts. From then on Factory only observes and never refreshes/heals it. Before this change, #superviseLocalMount would run ensureLocalMount → preflight → startMount() and auto-heal a Factory-owned mirror that later stalled; now it is permanently left to its (possibly absent) external supervisor. Only exempt mounts that are provably owned by a different supervisor (e.g. distinguishable via an identifier Factory never assigns), or re-evaluate ownership when the external daemon is absent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mount/relayfile-cloud-mount-client.ts, line 475:

<comment>The ownership probe treats any pre-existing healthy state (or a stale state whose pid is still alive) as externally managed, regardless of who launched the daemon. When Factory restarts, its own previously-spawned detached daemon (created via `ensureMountedWorkspace` with `background: true`) still writes a fresh `state.json` and `mount.pid`, and `#localMounts` is empty on the new process — so on the first `#ensureLocalMount` this branch adds it to `#externallyManagedLocalMounts`. From then on Factory only observes and never refreshes/heals it. Before this change, `#superviseLocalMount` would run `ensureLocalMount` → preflight → `startMount()` and auto-heal a Factory-owned mirror that later stalled; now it is permanently left to its (possibly absent) external supervisor. Only exempt mounts that are provably owned by a different supervisor (e.g. distinguishable via an identifier Factory never assigns), or re-evaluate ownership when the external daemon is absent.</comment>

<file context>
@@ -464,11 +469,22 @@ export class RelayfileCloudMountClient implements MountClient {
+    if (
+      !this.#localMounts.has(localDir) &&
+      staleBefore !== undefined &&
+      (!staleBefore.stale || isMountProcessRunning(staleBefore.pid))
+    ) {
+      this.#externallyManagedLocalMounts.add(localDir)
</file context>

) {
this.#externallyManagedLocalMounts.add(localDir)
}
if (staleBefore?.stale) this.#markLocalMountDegraded(localDir, 'mount_stale')

try {
await this.#localMountPreflight(this.workspaceId, join(localDir, '..'), {
...options,
// Attaching to a daemon owned by launchd/systemd (or another process)
// is read-only. Its supervisor owns recovery; an SDK replacement here
// would cancel that daemon and make the two supervisors fight.
...(this.#externallyManagedLocalMounts.has(localDir) ? { refreshStaleMount: false } : {}),
localDir,
acceptableWorkspaceIds: [...acceptableWorkspaceIds],
startMount: async () => {
Expand Down Expand Up @@ -573,6 +589,7 @@ export class RelayfileCloudMountClient implements MountClient {
this.#localMountSupervisions.clear()
this.#degradedLocalMounts.clear()
this.#authDegradedLocalMounts.clear()
this.#externallyManagedLocalMounts.clear()
const mounted = [...this.#localMounts.values()]
this.#localMounts.clear()
await Promise.allSettled(mounted.map(async (handle) => handle.stop()))
Expand Down Expand Up @@ -706,6 +723,7 @@ export class RelayfileCloudMountClient implements MountClient {
await mounted.stop()
return
}
this.#externallyManagedLocalMounts.delete(localDir)
this.#localMounts.set(localDir, mounted)
const supervision = this.#localMountSupervisions.get(localDir)
if (supervision) {
Expand Down Expand Up @@ -740,6 +758,22 @@ export class RelayfileCloudMountClient implements MountClient {
const supervision = this.#localMountSupervisions.get(localDir)
if (!supervision) return
try {
if (this.#externallyManagedLocalMounts.has(localDir)) {
// Health supervision for an attached daemon is deliberately
// observation-only. Keep reporting stale/recovered transitions while
// leaving all restart and cancellation decisions to its owner.
const statePath = join(localDir, '.relay', 'state.json')
const staleness = existsSync(statePath)
? checkMountStaleness(
statePath,
this.workspaceId,
this.#acceptableWorkspaceIds(supervision.options.acceptableWorkspaceIds),
)
: { stale: true, reason: `mount state is missing at ${statePath}` }
if (staleness.stale) this.#markLocalMountDegraded(localDir, 'mount_stale')
else this.#markLocalMountRecovered(localDir)
return
}
if (
supervision.suggestedRefreshAtMs !== undefined &&
Date.now() >= supervision.suggestedRefreshAtMs
Expand Down
Loading