Skip to content

Add PROOF_SINGLE_REPLICA mode to avoid false LIVE_DOC_UNAVAILABLE on single-replica self-hosts#58

Open
nishfaria wants to merge 1 commit into
EveryInc:mainfrom
nishfaria:single-replica-live-doc-gate
Open

Add PROOF_SINGLE_REPLICA mode to avoid false LIVE_DOC_UNAVAILABLE on single-replica self-hosts#58
nishfaria wants to merge 1 commit into
EveryInc:mainfrom
nishfaria:single-replica-live-doc-gate

Conversation

@nishfaria

Copy link
Copy Markdown

Problem

On a single-replica self-hosted deployment, all programmatic edits (/edit/v2, rewrite.apply) get blocked with LIVE_DOC_UNAVAILABLE for several minutes after any viewer opens a document — even once nobody is connected.

The live-doc mutation gate is designed for a multi-replica fleet. It treats a recent collab-session lease with no live connection on the current node as evidence that another replica holds the live document:

hostedRemoteLiveLease = collabRuntimeEnabled && hostedRuntime
  && breakdown.total > 0            // a recent session lease exists
  && breakdown.exactEpochCount === 0 // ...but not connected here

When a viewer disconnects, exactEpochCount drops to 0 but the session lease lingers for COLLAB_SESSION_TTL_SECONDS (default 300s). On a single node there is no sibling replica, so total > 0 && exactEpochCount === 0 simply means "nobody is connected" — yet the gate returns LIVE_DOC_UNAVAILABLE and blocks the edit. In practice this pushes agents to recreate documents instead of updating in place (which loses comments/marks anchored to the original).

isHostedRewriteEnvironment() is true whenever PROOF_ENV=production, so any production single-container deployment hits this.

Fix

Add an opt-in PROOF_SINGLE_REPLICA flag. When set, the gate trusts this node's own connection view instead of ghost leases.

  • rewrite-policy.ts: new isSingleReplicaDeployment() + derived isHostedMultiReplicaRewriteEnvironment().
  • canonical-document.ts: activeCollabClients derives from exactEpochCount (own connections) instead of total; the hostedRemoteLiveLease block is skipped when single-replica.
  • agent-edit-v2.ts: getStrictLiveClientCount / …WithGrace use exactEpochCount when single-replica.

Default behavior is unchanged when the flag is unset — multi-replica deployments keep the existing conservative gating.

Result (verified on a single-container deployment)

With PROOF_SINGLE_REPLICA=1:

  • /edit/v2 succeeds while a viewer is actively connected — the edit merges into the live Yjs document, and the connected browser receives it in real time (collab.status: confirmed).
  • A human comment added with a viewer present persists and survives subsequent agent edits.
  • No more LIVE_DOC_UNAVAILABLE false positives after a viewer disconnects.

Notes

  • Kept opt-in/explicit rather than auto-detecting (e.g. "no RAILWAY_REPLICA_ID"), since cold starts could misclassify; happy to switch to auto-detection if preferred.
  • Glad to add documentation (env var reference) and tests wherever you'd like them — didn't want to guess at the preferred location/format.

…single-replica self-hosts

The live-doc mutation gate assumes a multi-replica fleet: a recent collab-session
lease with no live connection on the current node (total > 0 &&
exactEpochCount === 0) is read as "another replica holds the live document" and
returns LIVE_DOC_UNAVAILABLE, blocking /edit/v2 (and rewrite.apply) until the
lease expires.

On a single-replica deployment there are no sibling nodes, so this is a false
positive: after any viewer disconnects, all programmatic edits are blocked for
the COLLAB_SESSION_TTL_SECONDS window (default 5 min), which pushes agents to
recreate documents instead of updating in place.

Add an opt-in PROOF_SINGLE_REPLICA flag (isSingleReplicaDeployment) plus a
derived isHostedMultiReplicaRewriteEnvironment helper. When single-replica:
- the canonical mutation derives activeCollabClients from exactEpochCount (this
  node's own connections) instead of ghost leases, and skips the
  hostedRemoteLiveLease block
- agent edit/v2 strict live-client count uses exactEpochCount

Default behavior is unchanged when the flag is unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant