Agent Coherence — Stale Write Guard (FS)
OfficialServer Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| SWG_ROOT | No | Root directory of the workspace to guard. Defaults to the current working directory. | (current working directory) |
| SWG_MANAGED | No | Comma-separated list of glob patterns to narrow which files are managed. If not set, the whole workspace is guarded. |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
stale-write-guard-fs guards a SINGLE-HOST workspace against silent lost updates when two or more agents share a mutable file. It enforces VERSION LINEAGE via a local coherence coordinator; it does NOT merge content for you.
Three guarantees, all single-host and fail-closed:
Sequential stale-overwrite — swg_write is DENIED (reason=stale_view) if the file changed since you read it: either a peer committed a newer version OR it was edited out-of-band (an editor, another tool, a shell script). Recover with swg_reacquire, then write FROM the fresh bytes it returns.
Concurrent same-key lost-update — swg_write_cas(path, expected_version, new_content) rejects a stale compare-and-set as a TYPED CONFLICT (not an auto-merge): you read, merge, and retry; the server never merges for you.
Effect fired on a superseded read — before ANY irreversible external action you decided from a swg_read (webhook, deploy, opened PR, posted message), call swg_gate(path, expected_version, expected_generation) with the pair that read returned. It DENIES (reason=stale_view) if the value moved OR the grant you read under was reclaimed while you were thinking — a reclaim leaves the version untouched, so the version alone cannot see it — OR a peer's write-claim preempted your grant, which leaves BOTH comparands untouched: the gate also re-checks that your grant still stands. On a deny, do not take the action: swg_reacquire, re-read, re-decide. The verdict is true as of that call; the dispatch after it is still yours.
WRITE HOLDS, in both modes: swg_write takes a write grant on the path it writes and keeps it after the write. swg_release(path) gives back that one path and keeps your other holds; swg_reacquire and swg_write_cas, of any path, drop every hold this session has, and so does this server's normal exit. When you finish writing a path, swg_release it: until you do, other opted-in agents that write it wait, for up to 30 minutes. swg_status lists your holds in held (null means it cannot tell, never that you hold nothing).
REFUSE MODE is the operator's setting, SWG_ON_HELD=refuse; swg_status reports the mode as on_held. In it, a swg_write of a managed path that another agent holds is refused with reason=other_holder, recover=wait_then_reacquire, retryable=false: nothing was written, nothing changed hands, and its next_step gives the wait. Any writer's grant counts as a hold, but only opted-in writers honour a hold, such as a refuse-mode server writing one of its managed paths: the Claude Code plugin's hooks, displace-mode servers (the default) and older volumes take the file anyway, and the holder then sees stale_view on its next swg_write. A hold is not a lock.
WHO HOLDS A PATH: holder ids are per attempt and never equal your session_agent_id, so tell your own holds by held_by in swg_status (this_attempt, earlier_attempt or another_agent), never by comparing ids. Every caller of one server process is one agent to the coordinator, so this_attempt can be another caller sharing this server.
HANDOFF between sessions: swg_transfer(paths, successor) hands this session's claim on each path (the write grant from swg_write, or the standing read from swg_read) to another session, the successor, named by the session_agent_id the successor's own swg_status reports. A transfer FENCES THE GIVER and DOES NOT RESERVE THE PATH: other sessions keep reading and writing it by the ordinary rules, and the handoff only labels what they do. While the handoff is live, this session's swg_write and swg_write_cas on the path are DENIED with reason=handed_off, retryable=false, recover=stop_and_report: stop and report to your user or host that the path was handed off. The fence covers this MCP session only: a write you make to the path through any other route (your own file tools, a shell) is another session's write and is not refused, so do not write a path you handed off by any route. The successor sees the handoff in the handoff key of its swg_read and swg_status, and takes it with swg_accept or a write, or refuses it with swg_decline. swg_withdraw is taken only on the user's or host's explicit instruction, never as the recovery for a handed_off deny.
REFUSALS are error results whose structuredContent carries reason, recover, retryable and detail, and often next_step: when next_step is present it is the procedure, so follow it. retryable=true means only that sending the identical call again is safe; it is not the next step. retryable=false means do not send it again unless next_step says to. The recover verbs: reacquire: swg_reacquire the path, then write FROM the bytes it returns. wait_then_reacquire: another agent holds the path; release your holds, poll swg_status, then swg_reacquire and swg_write once (next_step has the steps). write_again: the bytes swg_write put on disk may be unrecorded; swg_write the path once more, which records them. release_again: the release was not confirmed; swg_release the path again, at most twice more. reacquire_and_reconcile: the write lost its grant mid-write and its bytes may be on disk unrecorded; swg_reacquire, reconcile your change with the bytes it returns, and write that. read_then_merge: swg_read the path, merge into what it returns, then swg_write_cas at its version. reacquire_and_reread: swg_reacquire the path, then swg_read it for a fresh version. read_then_retry: the commit may have landed; swg_read the path and retry only if your change is not there. wait_and_retry: wait a few seconds, then send the same call again. wait_or_escalate: wait a while before trying again; if it persists, report it. retry_later: this server has no coordinator attached; try later, and report it if it persists. check_handoff: look at the path's handoff in swg_status before you act. fix_successor: name the successor by the session_agent_id its own swg_status reports. fix_path, check_path, use_text, reread: fix the path; check that the file exists; the file is not UTF-8 text; swg_read again and pass both comparands it returns. restart_session: this server session lost coordination; report it, since only a new server session regains it. stop, stop_and_report, none: stop, do not write the path by any other route, and report the detail to your user or host.
OUT OF GUARANTEE (do not rely on this server for): writers on DIFFERENT hosts or across a synced/network mount; divergent-history reconciliation; semantic/content correctness; any server-enforced auto-merge. These are NOT detected in v1 — a heterogeneous multi-host setup looks identical to a guarded one (swg_status reports heterogeneous_scope_detectable=false).
TRUST BOUNDARY: the server enforces that your write descends from a version you read; it CANNOT verify you derived your content from the bytes you read. Same-uid local processes can reach the coordinator directly, bypassing this server — the model is single-uid, single-host.
v1 guards UTF-8 TEXT artifacts (config, notes, memory, code); a non-text file is reported, not silently mangled (binary support → v1.1).
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| swg_readB | Read a workspace text file under coherence tracking. Returns {content, version, owner_generation}. The version is the comparand you pass to swg_write_cas; KEEP BOTH version and owner_generation and pass them to swg_gate before any irreversible external action you decide from this read (owner_generation=null means this coordinator does not report generations, so swg_gate will hold). When the path has a handoff record the result also carries handoff: the record as it concerns this session (role giver, successor or bystander, the two session_agent_ids, version_at_transfer, hold_shape, status, live); handoff_unknown=true instead means the read was denied and the record could not be fetched. The coordinator attaches the record best-effort, so a result with neither does not prove there is no record; swg_status lists every record. A sticky-INVALID view returns fresh bytes but stays INVALID — use swg_reacquire to recover before writing. If the bytes on disk are not the content at the current version, the read is DENIED with reason=stale_view and no version. A peer's commit still reaching disk clears on its own: swg_reacquire, then swg_read again, retrying for a few seconds. If it stays denied past that, the bytes on disk were never recorded (an out-of-band edit, a commit whose disk write failed, or a write whose commit did not reach the coordinator); reading again does not clear it: swg_write the content swg_reacquire returned (or your merge of it), then swg_read. A write made sooner can be overwritten by a peer commit still reaching disk. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_writeA | Write a workspace text file (acquire -> write -> commit). In either mode the write takes a write grant on the path and keeps it after the write. When you finish writing a path, swg_release it: until you do, other opted-in agents that write it wait, for up to 30 minutes. On a refuse-mode server (SWG_ON_HELD=refuse) a success also carries opted_in: true when the write asked first, false when the path is not among the managed files, so the write took the file from any agent that held it. A refusal carries reason, recover and retryable; follow its next_step when it has one. DENIED with reason=stale_view if the file changed since you read it — a peer commit OR an out-of-band edit (another tool/editor): recover with swg_reacquire, then write FROM its bytes, never the content you just sent. The other reasons: other_holder (refuse mode only), another agent holds the path and nothing was written (recover=wait_then_reacquire); commit_preempted, a mid-write preempt (disk may hold un-versioned bytes; reacquire_and_reconcile); commit_unconfirmed, the bytes are on disk but may be unrecorded (recover=write_again); link_out_of_managed (refuse mode only), the path goes through a link to a file outside the managed files; handed_off, this session handed the path off; coordinator_failed, the coordinator failed while answering (recover=reacquire when an earlier attempt of this session still holds the path); coordinator_unavailable, it gave no usable answer or is not attached; caller_principal_absent, caller_principal_foreign or caller_principal_claimed, this session's caller principal was refused (swg_status shows principal_claim); internal_error, a failure with no recovery; invalid_path or io_error, a bad path or a disk failure. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_reacquireA | Recover from a stale_view deny: re-mint identity and return the CURRENT bytes. You MUST write FROM these exact bytes — the server enforces version lineage, NOT that your content was derived from what you read. It is also the step a swg_write refusal names with recover=reacquire or wait_then_reacquire. The re-mint drops every hold this session has: the write grant on every path, not only this one, so other agents can then write those paths. To give back one path and keep the others, use swg_release. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_releaseA | Give back this session's write grant on one path and keep every other hold. swg_write takes a write grant on the path it writes and keeps it after the write. When you finish writing a path, swg_release it: until you do, other opted-in agents that write it wait, for up to 30 minutes. Answers {ok, path, released}. released=true means this session gave a grant back; it does not mean the path is free (another agent may hold it), and this session keeps a read at the version it held, so its next swg_write of the path needs no swg_reacquire unless the file changed since. released=false means this session had no write grant recorded on the path and nothing was sent, as on a repeat. A release the coordinator did not confirm answers reason=release_unconfirmed, recover=release_again, retryable=true: calling swg_release again is safe, at most twice more; if it still is not confirmed, report that this session's write grant on the path may still stand (swg_reacquire drops every hold, this one included). SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_statusA | Report coherence state: coordinator on|off|unknown (unknown is NOT off), per-path enforced|not_registered (with handoff: the path's handoff record -- giver, successor, version_at_transfer, hold_shape, status, live -- when it has one; per_path is null, not {}, when the coordinator's /status answer carries no list of tracked paths, as a degraded answer does when its registry was busy: retry shortly and do not treat it as nothing tracked; the text result then says per_path=unavailable. With coordinator=unknown, per_path is {} and says nothing about what is tracked), the write holder on each per_path entry someone holds for writing: holder_agent_id (the holder's attempt id, in the 32-hex spelling an other_holder refusal names; ids are per attempt, so it never equals session_agent_id) and held_by (this_attempt; earlier_attempt, an earlier attempt of this session whose grant still stands, which you release; or another_agent), neither key on an entry nobody holds, and this_attempt covering another caller sharing this server, since every caller of one server process is one agent to the coordinator; on_held (this server's mode: refuse when the operator set SWG_ON_HELD=refuse, else displace), held (the sorted paths this session holds for writing, either attempt; null when the coordinator cannot say who holds each path: null means cannot tell, never that nothing is held; the text result gives on_held, held, and a line per path held_by another_agent or earlier_attempt), is_attached/is_degraded/session_id, session_agent_id (this session's id as a handoff successor: the value another session passes to swg_transfer to hand this session a path; it does not change when this session reacquires, and it names this session as a successor only while principal_claim is bound), principal_claim (this session's caller-principal state: bound; unsupported = the coordinator issues none; unconfirmed = the last claim's answer was lost and the next call claims again by itself; refused = the session is bound under another nonce, so every later swg_read/swg_write/swg_gate answers the typed caller_principal_* deny with recover=restart_session while the coordinator stays on; not_attempted = nothing claimed yet), caller_principal_absent_total and caller_principal_refused_total (the coordinator's counters; null, never 0, when it is unreachable or does not report them), registry_schema_version (the schema version of the coordinator's store, an integer; an older release refuses a store at a newer version; null when the coordinator is unreachable (coordinator=unknown) or its answer carries no such field, as a coordinator built before the field does; report it to the user rather than act on it), and heterogeneous_scope_detectable=false (a multi-host or differently-scoped setup is NOT distinguishable in v1). SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_gateA | Verify a file is STILL unchanged and still under the same grant. Pass BOTH comparands from your earlier swg_read — expected_version AND expected_generation (its owner_generation). Call this immediately BEFORE any irreversible external action you decided from that read (sending a webhook, opening a PR, running a deploy, posting a message). Returns decision=proceed, or DENIES with reason=stale_view if the file moved OR the grant you read it under was reclaimed (the version alone cannot see a reclaim) OR a peer write-claim preempted your grant (which moves neither comparand — the gate re-checks that the grant still stands) OR either comparand is unconfirmed. On a deny: do NOT take the action — swg_reacquire, re-read, re-decide. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_write_casA | Concurrent same-key write via compare-and-set. You read (swg_read returns the version comparand; swg_reacquire does NOT — it is for swg_write recovery), MERGE, then call swg_write_cas(path, expected_version, new_content). Stale-write-rejected: if a peer committed since your read, the CAS is a TYPED CONFLICT (reason=version_mismatch, current_version returned) — NOT an auto-merge; re-read at current_version, re-merge, and retry. A win on a path with a live handoff carries handoff: outcome=completed when this session is its successor, overtaken (with counterparty) otherwise. The per-session conflict counter bounds only a COOPERATING agent (one session, stops on retryable=false); it is NOT livelock-proof against a fresh session or one that ignores retryable=false. A call that reaches the coordinator drops every hold this session has: it starts a new attempt, which gives back the write grant swg_write took on every path, as swg_reacquire does. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_transferA | Hand this session's claim on one or more paths -- the write grant from swg_write, or the standing read from swg_read -- to another session, the successor. Name the successor by the session_agent_id that the successor's OWN swg_status reports. Answers one grant per path, in order: transferred=true with the record (giver, successor, version_at_transfer, hold_shape, status=pending), or transferred=false with a typed reason and nothing changed (handoff_not_held: this session holds no claim on the path; handoff_successor_unknown: the coordinator does not know that id; handoff_in_flight: another session's handoff of the path is live). The result is an error unless every grant transferred, and each refused grant carries recover and retryable=false and a fixed next_step: fix_successor for a successor id that names no other session, check_handoff for handoff_not_held (a transfer that already landed answers it too), stop_and_report otherwise; the top level speaks for the most restrictive refused grant. While a handoff is live this session's swg_write and swg_write_cas on its path are denied with reason=handed_off. The fence covers this MCP session only: a write you make through any other route (your own file tools, a shell) is another session's and is not refused, so write a path you handed off by no route. If the answer is commit_unconfirmed (recover=check_handoff) the transfer may have landed: look at the path's handoff (swg_read or swg_status) first. A handoff from you to that successor made at the version you held (its version_at_transfer), live or ended, means your transfer landed: do not transfer again, and do not withdraw to start over. Transfer again only if no such handoff shows; if the path was written since or shows another session's handoff, ask your user. Once the successor has written the path, a second transfer is a new handoff. A transfer fences the giver and does not reserve the path: other sessions keep reading and writing it by the ordinary rules, and the handoff only labels what they do. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_acceptA | As the successor, accept the live handoff of a path without writing it: a pending handoff becomes completed (a write of the path completes it too). The giver stays fenced until a write moves the version. If another session has acquired the path and nothing has written it since, the handoff is overtaken but live: the accept is taken but changes nothing, answers status=overtaken and names that session as counterparty. Once a write has moved the version the handoff has ended, and the accept is refused handoff_not_live with the status saying how. Refused with handoff_not_successor or handoff_not_live, as an error that changes nothing (recover=stop_and_report, retryable=false, and a fixed next_step). A transfer fences the giver and does not reserve the path: other sessions keep reading and writing it by the ordinary rules, and the handoff only labels what they do. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_declineA | As the successor, decline the live handoff of a path: the handoff ends and its giver's fence lifts. Refused with handoff_not_successor or handoff_not_live, as an error that changes nothing (recover=stop_and_report, retryable=false, and a fixed next_step). A transfer fences the giver and does not reserve the path: other sessions keep reading and writing it by the ordinary rules, and the handoff only labels what they do. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
| swg_withdrawA | As the giver, withdraw this session's live handoff of a path: the handoff ends and this session's fence on the path lifts. Take it only on the user's or host's explicit instruction. It is never the recovery for the handed_off refusal: when a write of a path you handed off is refused, stop and report to your user or host. Refused with handoff_not_giver or handoff_not_live, as an error that changes nothing, whose next_step says to stop and report. A transfer fences the giver and does not reserve the path: other sessions keep reading and writing it by the ordinary rules, and the handoff only labels what they do. SINGLE-HOST only. Out of guarantee and NOT detected in v1: writers on different hosts or across a synced/network mount, divergent-history reconciliation, semantic correctness, server-enforced auto-merge. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 11 tools
Most tools have clearly distinct roles, but some boundaries require careful reading: swg_release vs swg_withdraw, swg_reacquire vs swg_read, and swg_write vs swg_write_cas all touch overlapping recovery/hold concepts. The descriptions do disambiguate them, so misselection is limited but plausible under time pressure.
All tools share the same swg_ prefix and lowercase snake_case convention, with predictable verb-based names such as swg_read, swg_write, swg_release, swg_transfer, swg_accept, and swg_decline. The noun-style swg_status and compound swg_write_cas are still consistent with the overall pattern.
Eleven tools is well within a reasonable range for a stateful filesystem coherence protocol with write tracking and handoff semantics. Each tool covers a distinct lifecycle step: status, read, write, CAS write, recover, release, gate, transfer, accept, decline, and withdraw.
The core guard lifecycle is well covered: read, write, CAS, recovery, release, pre-action gate, and the full handoff lifecycle. Minor gaps remain for general workspace file operations such as delete, rename, or list, but these may be intentionally outside the guard-focused scope.