Skip to content

relayfile: self-contained writebacks bypass bootstrap (SDK-configurable, surfaced via CLI; factory uses it automatically) #285

Description

@khaliqgant

Problem

Today the relayfile mount daemon's outbox flush is gated on bootstrap completion. Concretely: when the mount starts with status=bootstrapping and writebacks are already in the local .integrations/<provider>/.../factory-create-*.json path, those writebacks sit in the pending queue until the entire workspace tree has finished mirroring down.

For an operator workflow where the only goal is create a Linear issue from a JSON file, that means waiting many minutes for github/notion/slack/etc. to bootstrap before a single write fires — even though the create writeback is self-contained (its content includes a fresh UUID, full title + description + teamId + stateId — no dependency on existing workspace state).

Observed 2026-06-15 on workspace rw_7ccfea89:

  • Operator wrote two factory-create-<uuid>.json files to .integrations/linear/issues/.
  • Mount state showed status: bootstrapping, pendingWriteback: 2, lastSuccessfulReconcileAt: never — writes detected, not flushed.
  • After 5+ minutes, the mount had mirrored 95MB / 7,627 files (mostly github) and was still bootstrapping. The 2 writebacks remained queued.
  • The factory was running fine the entire time (it uses the cloud-API MountClient directly and never goes through the local mount). Only the operator's writeback workflow was blocked.

Workaround the operator tried: relayfile start ... --sync-mode write-only — but that flag is on the underlying Go binary and not surfaced by the user-facing relayfile start CLI (the wrapper only exposes --mode poll|fuse, which is something else). So today's escape hatch isn't actually reachable from the documented CLI surface.

This issue fixes both halves: the design (writes-precedence) and the surface (SDK + CLI knob).

Goal

A self-contained writeback (e.g. factory-create-<uuid>.json) lands at the cloud writeback queue within one sync cycle (~seconds) of being written locally, regardless of whether the mount has finished bootstrapping the rest of the workspace tree. Bootstrap continues in the background. Stateful writebacks (updates, comments) keep their current "wait for bootstrap" gate.

Design

The discriminator already exists

The relayfile CLI's writeback machinery already classifies writebacks by content identity — see relayfile/cmd/relayfile-cli/main.go:3290 writebackPushContentIdentity, which returns a contentIdentity only for known self-contained draft shapes:

  • factory-create-*.json (operator-supplied UUID, content is the full create intent)
  • relayfile.IsDraftFilePath(remotePath) (any provider's create-path drafts)

A contentIdentity is precisely the signal that says "this write does not depend on the local mount knowing the workspace state." The change is to use it as a bootstrap-bypass gate in the outbox flush loop.

Concrete change in the mount syncer

Today (relayfile/internal/mountsync/syncer.go outbox flush logic — approximate): the loop checks status != bootstrapping before draining outbox/pending/. Change to:

for _, op := range pending {
    if op.HasContentIdentity() {
        // Self-contained: safe to flush regardless of bootstrap state.
        flush(op)
        continue
    }
    if status == "bootstrapping" {
        continue  // existing behavior for stateful writebacks
    }
    flush(op)
}

The content-identity dedupe key (Kind:"mount-writeback-create-draft" Key:"<workspace>:<remotePath>:<contentHash>" with TTLSeconds:2592000) already protects against double-dispatch if the operator wrote the same file twice during the gap.

SDK-configurable

Expose this as a sync config option, not a hardcoded behavior, so operators / agents / the factory can opt out (e.g. for paranoid stateful workflows):

interface RelayfileMountConfig {
  // ... existing fields
  /**
   * Controls whether self-contained writebacks (factory-create-<uuid>.json,
   * other draft paths with content identity) flush during bootstrap.
   * - "self-contained": bypass bootstrap for writes with content identity;
   *   stateful writebacks still wait. (Default.)
   * - "wait":           today's behavior — all writebacks wait for bootstrap.
   * - "all":            flush every pending writeback immediately even
   *   without content identity. Use only when you know what you're doing.
   */
  writebackPrecedence?: "self-contained" | "wait" | "all"
}

Default to "self-contained" — it's the right behavior for ~all real workflows and matches the safety provided by the existing content-identity dedupe.

CLI surfacing

The user-facing relayfile start wrapper currently only exposes --mode poll|fuse. Add:

--writeback-precedence self-contained|wait|all   (default: self-contained)
--sync-mode mirror|write-only                    (already on the Go binary; surface in wrapper)

The wrapper translates these to the equivalent Go binary flags. Both flags are useful for distinct operator scenarios:

  • --writeback-precedence self-contained — the default, fixes today's bug
  • --sync-mode write-only — even more aggressive: don't mirror down at all, only push local changes (useful when the operator only wants to create issues and doesn't care about reading workspace state)

Factory launcher uses it automatically

The companion pear-side issue (linear-issue-pear-factory-auto-mount-integrations.md — the auto-mount-on-factory-start work) should pass --writeback-precedence self-contained (the default, but explicit is good) when launching the operator-side mount. That way the first factory-create-*.json an operator (or me, as an agent) writes during a factory session lands within seconds, not minutes.

The same option should be configurable via the factory-sdk so the launcher doesn't hardcode CLI flag names — e.g. factory.config.json could carry mount.writebackPrecedence and the launcher translates.

End-to-end verification — required during development

  1. Cold mount with no local state. Pre-seed two factory-create-<uuid>.json files in .integrations/linear/issues/. Start relayfile start ws-id .integrations --background --rehome with default config. Confirm both writebacks reach the cloud within 60 seconds, even though status is still bootstrapping and lastSuccessfulReconcileAt is still never. Capture: timestamps on creation, timestamps on outbox/acked entries.
  2. Same setup, but write a stateful update writeback (e.g. an issue update against an existing AR-NNN path). Confirm it stays queued until bootstrap completes — pendingWriteback > 0 throughout bootstrap, then drops to 0 once status=synced.
  3. With --writeback-precedence wait flag, confirm today's behavior is preserved (no regression for paranoid operators).
  4. With --sync-mode write-only surfaced via the wrapper, confirm bootstrap is skipped entirely and writebacks still flush.
  5. End-to-end via the factory launcher (after the pear-side auto-mount issue lands): pear factory start --mode live in a fresh shell, write a factory-create-*.json within 30 seconds of factory start, confirm it lands in Linear. Capture Linear AR-NNN identifier in the PR.

Acceptance criteria

  1. writebackPrecedence config option added to the SDK with three values + sensible default of self-contained.
  2. --writeback-precedence and --sync-mode surfaced as flags on the user-facing relayfile start CLI wrapper (not just the Go binary).
  3. Outbox flush loop in mountsync/syncer.go (or equivalent) gates on content-identity for the bypass path.
  4. Content-identity dedupe (existing) confirmed sufficient to prevent double-dispatch on bootstrap-bypass writes.
  5. E2E demo: a factory-create-*.json reaches Linear within 60 seconds of being written, against a cold mount.
  6. No regression on stateful writebacks (updates, comments) — they continue to wait for the relevant subtree to bootstrap.
  7. Tests in relayfile/internal/mountsync/syncer_test.go cover the bypass path with both self-contained and stateful writebacks.
  8. The pear-side auto-mount issue (linear-issue-pear-factory-auto-mount-integrations.md) is updated/closed-against this work to confirm the launcher uses the new default.

Out of scope

  • Adding new draft path shapes. This issue uses the existing writebackPushContentIdentity classifier as-is.
  • Changing the bootstrap algorithm itself. This issue only changes when the outbox flushes.
  • Long-lived operator session tokens (separate paired issue) — orthogonal but synergistic.
  • The cloud-side by-state stale event emission bug (separate paired issue).

Related

  • relayfile/cmd/relayfile-cli/main.go:3290writebackPushContentIdentity (the existing discriminator).
  • relayfile/internal/mountsync/syncer.go — outbox flush loop (where the bypass gate lives).
  • pear/linear-issue-pear-factory-auto-mount-integrations.md — companion issue; the factory launcher will set this option.
  • pear/linear-issue-relayfile-long-lived-operator-session-tokens.md — orthogonal credential-lifecycle improvement, same operator-workflow class.
  • Observed 2026-06-15 on rw_7ccfea89: 2 self-contained writebacks queued for 5+ minutes during cold bootstrap (95MB / 7,627 files mirrored, still incomplete).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions