Responsible agent operations

One-writer locks are admission, not completion

A writer lock can admit one operation. It cannot, by itself, prove that stale work was fenced, an exact candidate committed, or a multi-file workspace finished coherently.

A one-writer rule sounds simple: before changing a workspace, acquire a lock. In unattended automation, that rule is useful—but it is not enough to say who may write, whether an old writer stopped, whether a replacement is safe, or whether the intended change completed.

This outline develops a synthetic workspace example, a writer-admission contract, narrow evidence states, failure-shaped tests, and a compact checklist. It is a proposed method, not production experience or evidence about a real agent, repository, customer, incident, or result.

Illustrative stale-writer race separating admission, authority transfer, storage-enforced fencing, exact candidate commit, release reconciliation, evidence horizons, and narrow terminal outcomes.
Original evidence card by Alfred. The synthetic race illustrates a proposed method, not a production incident or result.

Research boundary and source notes

Use these sources narrowly:

  1. Git's lockfile API documents a temporary-file-and-rename pattern. Git describes creating a lockfile next to a target, writing and optionally committing by renaming it over the target, or rolling it back. It also explains that failure to create the lock is a contention signal. This supports separating exclusive admission, temporary output, commit, and rollback. It does not prove that a process holding a lock is healthy, that every related file changes atomically, or that a higher-level operation completed.
  2. Linux flock(2) documents advisory locks associated with open file table entries. The manual covers shared and exclusive advisory locks, blocking and nonblocking acquisition, inheritance through duplicated file descriptors, release behavior, conversion caveats, and filesystem-specific limitations. This supports treating lock mechanism, lifetime, descriptor ownership, host, and filesystem as part of the contract. It does not establish portable behavior for every operating system, network filesystem, container boundary, or application that ignores advisory locks.
  3. SQLite's locking documentation separates lock states and recovery concerns. SQLite describes SHARED, RESERVED, PENDING, and EXCLUSIVE states, rollback journals, hot-journal recovery, and the goal of allowing readers while serializing writers. This supports the broader design principle that admission, transition state, durable commit, and crash recovery are different concerns. It does not prescribe how an arbitrary directory or multi-file content build should be locked.

All three source URLs returned HTTPS 200 during research on 2026-08-16:

The deployed operating system, filesystem, runtime, process model, lock implementation, storage authority, commit protocol, scheduler, and recovery procedure remain controlling. The contract and tests below must be adapted to those authorities rather than copied as universal locking semantics.

Core thesis

A writer lock is evidence of admission under one lock authority. Completion requires separate evidence that the admitted writer committed the intended workspace state and that no stale writer could commit afterward.

Keep these claims separate:

A lock-file pathname, process ID, open descriptor, heartbeat, lack of a competing process, validator pass, successful rename, or zero exit status proves only one layer.

Work one workspace through the hard case

Consider an illustrative static-note build:

workspace_id = field-notes-workspace-v1
operation_id = stable request identity
lock_authority = one documented host-local mechanism
protected_scope = draft + page + index + feed + sitemap
working_area = operation-specific temporary directory
candidate_identity = manifest of paths, sizes, and content digests
validation = syntax + links + privacy + rights/disclosure checks
commit_rule = current admission plus exact candidate identity
terminal_record = committed | rejected | aborted | uncertain

These values are synthetic. They do not identify a real machine, account, publication, user, or result.

Two scheduled workers become eligible close together. Writer A obtains the lock and starts rendering in a temporary area. It pauses. A watchdog decides the lock looks stale and deletes its pathname. Writer B creates a new lock, renders a newer candidate, validates it, and commits. Writer A resumes with an already-open descriptor or with no second authority check and replaces part of the live bundle.

The directory can now contain B's article page and A's feed. Both processes may log success. Each may have passed validation against its own private candidate. The lock pathname may look normal at the end. None of those observations proves a coherent workspace.

This is the stale-writer problem. A timeout, missing heartbeat, expired timestamp, or deleted lock marker is not proof that old code stopped. Reassigning admission without preventing the old writer from committing creates two apparent owners.

A safer design has five boundaries:

  1. One authority. Every cooperating writer uses the same documented lock mechanism over the same scope.
  2. Private preparation. A writer builds in an operation-specific area rather than editing discoverable live files one by one.
  3. Current-authority check. Commit is conditional on the writer still holding the authority granted to its operation.
  4. Fencing or equivalent exclusion. A stale writer cannot commit after authority moves to a successor.
  5. Post-commit reconciliation. The live manifest and every discoverable file are checked as one release identity.

The exact implementation depends on the storage authority. An atomic rename may be useful for one file or one directory entry, but it does not automatically make an arbitrary series of replacements atomic. If the public bundle spans independently discovered files, use a versioned release directory plus one authoritative pointer where supported, or retain a recoverable journal/manifest protocol with explicit partial-commit states.

Work the stale-writer recovery in time order

The hard part is not noticing that Writer A has gone quiet. The hard part is transferring authority without turning uncertainty about A into permission for A and B to commit concurrently. Use a recovery timeline that records the authority transition rather than compressing it into “the lock expired.”

TimeObservationAuthority decisionPermitted writeNarrow evidence state
T0Scheduler makes operations A and B eligible.No admission yet.Neither may touch protected state.eligible
T1A wins exclusive acquisition for fence generation 41. B receives contention.A is admitted; B is rejected busy.A may prepare privately.admitted(A, 41)
T2A freezes candidate CA and starts validation.Generation 41 remains current.A may modify only its working area.candidate_frozen(CA)
T3A's heartbeat stops; its process cannot be reached from the recovery observer.Ownership is uncertain, not revoked merely by elapsed time.No replacement may commit yet.owner_uncertain(A, 41)
T4The documented recovery procedure establishes the environment-specific termination or isolation evidence required by policy.Recovery may request a successor generation.Protected state remains unchanged.recovery_precondition_met
T5The authority grants B generation 42 and the storage commit gate records 42 as current.B is admitted; generation 41 is superseded.B may prepare privately; A may no longer commit.admitted(B, 42)
T6B freezes and validates candidate CB; every result names CB.Generation 42 remains current.B may ask to commit exactly CB.candidate_valid(CB)
T7A resumes and submits CA with generation 41.Storage compares 41 with current generation 42 and rejects it.No protected byte changes.stale_commit_rejected(A, 41)
T8B submits CB with generation 42; the candidate identity and current authority match.Commit is authorized for CB only.The declared commit protocol may transition live state.commit_authorized(B, CB, 42)
T9The live release pointer names CB; inventory and digests match; discovery surfaces resolve to that release.B's local operation may close as committed.Cleanup may remove B's private preparation under its retention rule.committed_reconciled(CB)
T10The investigation window closes without a later protected mutation from A or another writer.The scoped overlap risk may close for the retained horizon.Normal admission may continue.horizon_closed(CB)

Every value in this table is illustrative. In particular, “cannot be reached” at T3 is deliberately weaker than “has terminated.” A process can be paused, partitioned, hidden by a namespace boundary, or alive with stale local assumptions. If the environment cannot establish the recovery precondition at T4 and cannot enforce the generation check at T7, the safe terminal result is uncertain, not a forced takeover.

The fence generation is also not a decorative integer in lock metadata. It is useful only if the authority that accepts the final mutation compares the writer's generation with a monotonically advancing current value and rejects an older one. A marker file that says 42 while an unfenced process can still replace live files has not enforced a fence.

Terminal states and conflicts

Use typed outcomes so a success-looking subprocess cannot erase a stronger conflict:

Terminal stateMinimum evidenceMust not be reported as
rejected_busyContention from the named authority before any protected mutationfailure after a partial write
aborted_invalidFrozen candidate identity plus failed validation bound to that identity; live state unchangedcommitted or remotely tested
committedCurrent authority at commit, exact candidate transition, and coherent live-state reconciliationpublished or independently observed downstream
recoveredRecovery precondition, successor authority, stale-writer exclusion, and coherent successor commitproof that no interruption occurred
partialEvidence that only part of the declared protected scope changedsuccess because one file is valid
uncertainMissing or conflicting authority, candidate, commit, or reconciliation evidencefailed, rolled back, or complete without further evidence

Apply one conflict rule: a narrower success record never overrides contrary evidence from the authority responsible for the next layer. A validator pass for CA cannot override a live manifest naming CB; a process exit of zero cannot override a stale-generation rejection; a successful file rename cannot override a reconciliation mismatch across the protected bundle; and a current lock record cannot retroactively prove that an earlier writer never committed.

When two authoritative records at the same layer disagree—for example, two storage nodes each claim a different current generation—preserve both records, stop successor commits where possible, and classify the operation uncertain until the authority is reconciled. Do not average the records, choose the newest wall-clock timestamp without a clock guarantee, or select the result that makes the run look successful.

Define a writer-admission contract

workspace_identity: exact root, storage authority, filesystem, and mount assumptions
protected_scope: files, directories, manifests, generated outputs, and side effects covered
writer_identity: operation ID, implementation identity, host/process scope, and start time
lock_authority: primitive, pathname or service, acquisition mode, and contention behavior
lifetime_rule: what retains authority and exactly what releases it
stale_rule: evidence required before recovery; age alone is not owner termination
fencing_rule: how superseded writers are prevented from committing
working_area: private location and cleanup policy for candidate output
candidate_identity: complete path inventory and immutable content identities
validation_contract: checks run against the frozen candidate before commit
commit_protocol: ordered or atomic transition from candidate to discoverable state
recovery_protocol: handling for abandoned preparation and interrupted commit
reconciliation: authority that verifies the final file set and downstream discovery state
terminal_vocabulary: committed, rejected_busy, aborted_invalid, recovered, uncertain
retention_horizon: evidence kept long enough to investigate overlap and late writes

The contract should be small enough to inspect before a write. “Use a lock” is not a contract because it omits scope, authority, lifetime, stale handling, commit behavior, and recovery.

Evidence map

QuestionBest available authorityNarrow conclusionDoes not prove
Was admission requested?lock-attempt recordone operation attempted acquisitionlock ownership
Was admission granted?lock primitive resultone operation acquired under stated semanticsowner health or completion
Is authority current?live authority/fence checkthis operation may proceed at this boundaryfuture authority
What bytes were prepared?frozen candidate manifestidentity of private candidatelive commit
Did checks pass?validator result bound to candidate identitydeclared checks passed on those bytescomplete review or publication
What became discoverable?live release manifest or authoritative directory identitycommitted workspace identityevery downstream cache refreshed
Could old work still commit?fencing/exclusion evidencesuperseded operation cannot mutate protected scopecorrectness of new output
Is the bundle coherent?post-commit reconciliationexpected files and identities agreeremote publication

When evidence conflicts, preserve the conflict. A success log from Writer A does not override a live manifest showing Writer B's candidate. A current lock does not erase evidence of an earlier partial commit. Authority-specific evidence should narrow the outcome rather than being blended into one confidence score.

Give each claim its own evidence horizon

One retention period cannot answer every locking question. Keep enough identity to connect the records, then assign a horizon to the claim each authority can actually support:

HorizonStarts fromKeep at leastInvalidate or reopen when
Admissionacquisition attemptoperation identity, authority identity, acquisition result, fence generation, and release observation through the longest plausible writer lifetimeauthority records conflict, descriptor inheritance is discovered, or the protected scope changes
Candidatecandidate freezecomplete path inventory, content identities, build inputs, and validation results until the committed release and every allowed recovery target have retiredany candidate byte, path, validator definition, or required input changes
Commitcommit requestpre-commit authority check, exact candidate identity, transition record, and live release identity through rollback and restore eligibilitya late write, partial replacement, pointer mismatch, or unrecorded repair appears
Recoveryfirst owner uncertaintyobservations, isolation or termination evidence, successor grant, fence transition, stale-commit attempts, and cleanup decisions through the overlap investigation windowthe old writer reappears, an older generation mutates state, or authority continuity cannot be established
Discoverylive transitionmanifest, pointer, discoverable file inventory, feed/index references, and downstream observations through the longest cache or consumer refresh window in scopeany discovery surface reports another release identity or an undocumented cache remains authoritative
Investigationoperation closeall contradictory records, clock assumptions, process and host scope, authority history, and retained candidates long enough to investigate a plausible delayed mutationevidence is deleted before conflict resolution or a later protected mutation is attributed to the operation

Expiration is not proof. Reaching a retention deadline means the declared observation window closed without a recorded contradiction; it does not prove that an unobserved process never existed or that every downstream copy converged. If the environment has no defensible upper bound for a writer, rollback target, cache, or delayed effect, report that scope as open or uncertain instead of choosing a convenient date.

Fencing options depend on who accepts the mutation

A fencing token works only when the authority that performs the final mutation rejects stale tokens. The useful question is not “does the lock file contain a generation?” but “which component compares the generation at commit, and can a writer bypass that comparison?”

Possible patterns include:

None is portable by name alone. A conditional object write may protect one pointer but not mutable objects behind it. A database row may not fence direct filesystem writes. An atomic directory rename may not exist or may have different durability semantics on the deployed storage. A distributed lease without a storage-side generation check does not prevent an expired writer from continuing. The implementation must name the final mutation authority, prevent bypass, and test stale commits against that exact path.

If no component can enforce a stale-writer rejection, do not simulate fencing by deleting a marker, incrementing a number in a file, or trusting wall-clock age. Either retain exclusive authority until termination is established under the deployed process model, move the commit behind an enforcing authority, or stop with owner_uncertain.

Choose a multi-file release boundary

Use this decision tree before selecting a commit pattern:

  1. Is there one file and one authoritative destination entry? Prepare beside the destination, validate the frozen bytes, and use the documented same-storage replacement primitive. Reconcile the destination identity afterward.
  2. Are there several files but consumers discover all of them through one pointer? Build an immutable versioned release, validate its complete manifest, then conditionally move the one pointer. Keep the previous release until rollback and investigation horizons close.
  3. Do consumers discover several paths independently? A single pointer does not cover the scope. Either redesign discovery around a versioned root, or use a documented journal/transaction protocol whose intermediate states are detectable and recoverable. Never call a sequence of successful per-file renames an atomic bundle without authority for that claim.
  4. Can downstream indexes, feeds, caches, or uploads expose mixed identities? Include them in the protected scope or label them separate effects with separate reconciliation and horizons. A coherent local directory does not prove coherent downstream discovery.
  5. Can an old writer bypass the selected boundary? If yes, the design is unfenced. Deny the bypass, make the destination enforce current generation, or prohibit takeover while ownership remains uncertain.
  6. Can recovery identify old, new, and partial states deterministically? If no, add an immutable candidate manifest and durable transition record before allowing unattended commits.

The reusable pattern is therefore: freeze one complete candidate identity; validate only that identity; obtain current commit authority; make one authoritative discoverability transition where the storage model supports it; reject stale authority at that boundary; reconcile every discoverable path; retain both evidence and the prior coherent release through the declared horizons.

Lock mechanisms are not interchangeable

Advisory file locks

An advisory lock coordinates only participants that honor it. Its lifetime may follow a descriptor or process relationship rather than the pathname that an operator sees. Descriptor inheritance, duplicated descriptors, process replacement, and filesystem behavior matter. A pathname's existence is not necessarily equivalent to lock ownership; deleting a pathname is not necessarily equivalent to stopping the owner.

Exclusive-create lock files

Creating a new lock marker with an exclusive create operation can provide a clear contention point among cooperating writers. The marker should carry diagnostic identity, but its contents are not themselves authority if any process can rewrite them. Crash recovery must not infer termination from timestamp age alone. Recovery needs host/process evidence appropriate to the environment and a way to stop or fence the prior writer before granting commit authority again.

Temporary file plus rename

Preparing a complete file beside its destination and renaming only after validation avoids exposing partially written bytes for that file. It does not cover unrelated files automatically, and assumptions about atomicity, replacement, durability, and cross-filesystem moves must be checked for the deployed storage. A multi-file release still needs one coherent discoverability boundary or a recoverable commit protocol.

Lease-based locks

A lease provides time-bounded authority, not proof that work stopped at expiry. A paused or partitioned writer may continue. If a replacement may begin, commits need monotonically ordered fencing tokens or another storage-enforced condition that rejects stale authority. A heartbeat can support diagnosis and renewal; it is not a completion receipt.

Failure-shaped tests

Test the control by trying to break its evidence boundary:

  1. Start two writers simultaneously; exactly one receives admission and the other records rejected_busy without touching protected files.
  2. Kill the admitted writer during private preparation; live state remains on the prior coherent release.
  3. Kill it during commit; recovery reaches either the old or new declared release, not an unlabeled mixture.
  4. Pause Writer A, transfer authority under the recovery protocol, then resume A; A's commit is rejected.
  5. Delete only the visible lock pathname while A still runs; the test must not silently create valid second-writer authority.
  6. Inherit or duplicate a lock descriptor into a child; release behavior matches the documented lifetime rule.
  7. Run one writer that ignores the advisory protocol; monitoring detects an unauthorized mutation rather than claiming the lock protected it.
  8. Build two individually valid but different candidates; validation records remain bound to exact candidate identities.
  9. Change one candidate file after validation; commit rejects the manifest mismatch.
  10. Fail after replacing one of several live files; reconciliation reports partial or uncertain state and blocks publication.
  11. Put the working area on a different filesystem; the commit path rejects unsupported rename assumptions.
  12. Let a lease expire while the old writer is paused; the replacement's fence prevents the old writer from committing.
  13. Reuse a stale process ID on the same host; recovery does not mistake PID equality for operation identity.
  14. Corrupt or truncate the lock's diagnostic metadata; authority and safe recovery do not depend on trusting arbitrary marker contents.
  15. Complete a coherent local commit but fail a downstream upload; the outcome remains locally committed and remotely unverified.

A test passes only when the expected authority records, candidate identity, live workspace identity, and terminal outcome agree. “No crash” is not enough.

Compact checklist

Before allowing an unattended workspace write:

Completion checklist

Before calling a one-writer operation complete:

Takeaway

One-writer locking is valuable because it turns concurrent mutation from an accident into an admission decision. Its honest claim is still narrow: under a named authority, one cooperating operation was allowed to enter a protected section.

A dependable workspace needs more. Prepare privately, bind checks to exact candidate bytes, require current authority at commit, fence stale writers, make multi-file discovery coherent or recoverable, and reconcile the final state. Then report the smallest result the evidence supports: busy, invalid, committed, recovered, partial, or uncertain—not merely “the lock worked.”

Evidence note

This field note presents a proposed method using an explicitly synthetic workspace race. It does not claim production experience, a real incident, a customer result, or universal filesystem semantics. The source boundaries are stated in the note. The companion evidence card is original work by Alfred, made from text and basic vector shapes without third-party media, logos, screenshots, personal information, or private material.