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.
Research boundary and source notes
Use these sources narrowly:
- 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.
- 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. - 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:
- Git, Lockfile API
- Linux man-pages,
flock(2) - SQLite, File Locking And Concurrency In SQLite Version 3
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:
- Policy declared: documentation says that only one writer should run.
- Mechanism available: a lock primitive exists on the active host and filesystem.
- Acquisition attempted: one candidate asked the lock authority for admission.
- Admission granted: the authority granted exclusive access for a named operation.
- Admission current: the writer still holds valid authority at the moment of commit.
- Mutation started: the admitted writer began preparing changes.
- Candidate validated: temporary output passed the declared checks.
- Commit authorized: current authority allowed the exact candidate to replace live state.
- Commit observed: the workspace authority reports the intended new identity.
- Old work fenced: an earlier writer cannot commit after losing authority.
- Effects reconciled: the expected file set, manifest, and downstream state agree.
- Outcome verified: the scoped operation is complete through its evidence horizon.
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:
- One authority. Every cooperating writer uses the same documented lock mechanism over the same scope.
- Private preparation. A writer builds in an operation-specific area rather than editing discoverable live files one by one.
- Current-authority check. Commit is conditional on the writer still holding the authority granted to its operation.
- Fencing or equivalent exclusion. A stale writer cannot commit after authority moves to a successor.
- 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.”
| Time | Observation | Authority decision | Permitted write | Narrow evidence state |
|---|---|---|---|---|
| T0 | Scheduler makes operations A and B eligible. | No admission yet. | Neither may touch protected state. | eligible |
| T1 | A wins exclusive acquisition for fence generation 41. B receives contention. | A is admitted; B is rejected busy. | A may prepare privately. | admitted(A, 41) |
| T2 | A freezes candidate CA and starts validation. | Generation 41 remains current. | A may modify only its working area. | candidate_frozen(CA) |
| T3 | A'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) |
| T4 | The 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 |
| T5 | The 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) |
| T6 | B freezes and validates candidate CB; every result names CB. | Generation 42 remains current. | B may ask to commit exactly CB. | candidate_valid(CB) |
| T7 | A 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) |
| T8 | B 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) |
| T9 | The 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) |
| T10 | The 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 state | Minimum evidence | Must not be reported as |
|---|---|---|
rejected_busy | Contention from the named authority before any protected mutation | failure after a partial write |
aborted_invalid | Frozen candidate identity plus failed validation bound to that identity; live state unchanged | committed or remotely tested |
committed | Current authority at commit, exact candidate transition, and coherent live-state reconciliation | published or independently observed downstream |
recovered | Recovery precondition, successor authority, stale-writer exclusion, and coherent successor commit | proof that no interruption occurred |
partial | Evidence that only part of the declared protected scope changed | success because one file is valid |
uncertain | Missing or conflicting authority, candidate, commit, or reconciliation evidence | failed, 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
| Question | Best available authority | Narrow conclusion | Does not prove |
|---|---|---|---|
| Was admission requested? | lock-attempt record | one operation attempted acquisition | lock ownership |
| Was admission granted? | lock primitive result | one operation acquired under stated semantics | owner health or completion |
| Is authority current? | live authority/fence check | this operation may proceed at this boundary | future authority |
| What bytes were prepared? | frozen candidate manifest | identity of private candidate | live commit |
| Did checks pass? | validator result bound to candidate identity | declared checks passed on those bytes | complete review or publication |
| What became discoverable? | live release manifest or authoritative directory identity | committed workspace identity | every downstream cache refreshed |
| Could old work still commit? | fencing/exclusion evidence | superseded operation cannot mutate protected scope | correctness of new output |
| Is the bundle coherent? | post-commit reconciliation | expected files and identities agree | remote 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:
| Horizon | Starts from | Keep at least | Invalidate or reopen when |
|---|---|---|---|
| Admission | acquisition attempt | operation identity, authority identity, acquisition result, fence generation, and release observation through the longest plausible writer lifetime | authority records conflict, descriptor inheritance is discovered, or the protected scope changes |
| Candidate | candidate freeze | complete path inventory, content identities, build inputs, and validation results until the committed release and every allowed recovery target have retired | any candidate byte, path, validator definition, or required input changes |
| Commit | commit request | pre-commit authority check, exact candidate identity, transition record, and live release identity through rollback and restore eligibility | a late write, partial replacement, pointer mismatch, or unrecorded repair appears |
| Recovery | first owner uncertainty | observations, isolation or termination evidence, successor grant, fence transition, stale-commit attempts, and cleanup decisions through the overlap investigation window | the old writer reappears, an older generation mutates state, or authority continuity cannot be established |
| Discovery | live transition | manifest, pointer, discoverable file inventory, feed/index references, and downstream observations through the longest cache or consumer refresh window in scope | any discovery surface reports another release identity or an undocumented cache remains authoritative |
| Investigation | operation close | all contradictory records, clock assumptions, process and host scope, authority history, and retained candidates long enough to investigate a plausible delayed mutation | evidence 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:
- Conditional pointer update. Candidates live under immutable release identities. A storage service changes one authoritative release pointer only when its current version matches the writer's expected version. A stale compare-and-swap loses without changing discoverable state.
- Generation-checked commit service. Writers submit the candidate identity and fence generation to one service. The service serializes commits and rejects generations older than its current durable value. Direct writes around that service must be denied, not merely discouraged.
- Transactional metadata row. A database transaction checks current ownership and generation, records the committed candidate, and advances the release identity together. This can fence metadata changes only if protected bytes cannot become live through another path and recovery preserves the same authority.
- Versioned directory plus authoritative pointer. A writer prepares a complete immutable directory, validates it, then conditionally changes one discoverability pointer. This reduces multi-file exposure, subject to the filesystem or storage service's documented rename, replacement, durability, and conditional-write semantics.
- Host-local exclusion with no takeover. A descriptor-backed lock can be sufficient for one host and cooperating processes when the policy never grants a successor until the operating system has released the old authority. This avoids pretending that a diagnostic generation is storage-enforced, but it may require stopping safely rather than recovering availability immediately.
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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- Start two writers simultaneously; exactly one receives admission and the other records
rejected_busywithout touching protected files. - Kill the admitted writer during private preparation; live state remains on the prior coherent release.
- Kill it during commit; recovery reaches either the old or new declared release, not an unlabeled mixture.
- Pause Writer A, transfer authority under the recovery protocol, then resume A; A's commit is rejected.
- Delete only the visible lock pathname while A still runs; the test must not silently create valid second-writer authority.
- Inherit or duplicate a lock descriptor into a child; release behavior matches the documented lifetime rule.
- Run one writer that ignores the advisory protocol; monitoring detects an unauthorized mutation rather than claiming the lock protected it.
- Build two individually valid but different candidates; validation records remain bound to exact candidate identities.
- Change one candidate file after validation; commit rejects the manifest mismatch.
- Fail after replacing one of several live files; reconciliation reports partial or uncertain state and blocks publication.
- Put the working area on a different filesystem; the commit path rejects unsupported rename assumptions.
- Let a lease expire while the old writer is paused; the replacement's fence prevents the old writer from committing.
- Reuse a stale process ID on the same host; recovery does not mistake PID equality for operation identity.
- Corrupt or truncate the lock's diagnostic metadata; authority and safe recovery do not depend on trusting arbitrary marker contents.
- 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:
- ☐ Name the exact workspace and protected file set.
- ☐ Confirm every cooperating writer uses one lock authority.
- ☐ Record a stable operation identity separate from a process ID.
- ☐ Document acquisition, contention, lifetime, and release semantics.
- ☐ Treat advisory locks as advisory.
- ☐ Build in an operation-specific private area.
- ☐ Freeze a complete candidate manifest before validation.
- ☐ Bind every check result to that candidate identity.
- ☐ Recheck current authority at the commit boundary.
- ☐ Prevent a superseded writer from committing.
- ☐ Do not treat lease expiry, old timestamps, or missing heartbeats as termination proof.
- ☐ Verify atomicity and durability assumptions on the deployed filesystem.
- ☐ Give a multi-file release one coherent discovery boundary or recoverable journal.
- ☐ Test crashes before and during commit.
- ☐ Reconcile the live file set after commit.
- ☐ Preserve typed busy, invalid, recovered, partial, and uncertain outcomes.
- ☐ Keep lock diagnostics free of secrets and unnecessary personal data.
- ☐ Separate local commit from upload, listing, and public verification.
Completion checklist
Before calling a one-writer operation complete:
- ☐ Name the exact workspace, storage authority, filesystem assumptions, and protected scope.
- ☐ Give the operation a stable identity that is not only a PID, hostname, or timestamp.
- ☐ Confirm every cooperating writer uses the same admission authority.
- ☐ Document acquisition, contention, lifetime, inheritance, conversion, and release behavior.
- ☐ Treat a missing heartbeat or old timestamp as owner uncertainty, not termination proof.
- ☐ Build in an operation-specific private area and freeze a complete candidate manifest.
- ☐ Bind every validation result to the frozen candidate identity.
- ☐ Select one documented multi-file discovery or recovery boundary.
- ☐ Recheck current authority immediately before commit.
- ☐ Make the final mutation authority reject stale generations, or prohibit takeover until release is established.
- ☐ Deny direct write paths that bypass the commit authority.
- ☐ Test simultaneous acquisition and require the loser to leave protected state untouched.
- ☐ Test termination during preparation and during the discoverability transition.
- ☐ Pause an old writer, transfer authority under the recovery protocol, resume it, and observe stale-commit rejection.
- ☐ Reconcile the live manifest, file inventory, discovery surfaces, and downstream effects separately.
- ☐ Preserve contradictory evidence and use typed
partialoruncertainoutcomes. - ☐ Retain admission, candidate, commit, recovery, discovery, and investigation evidence through their own horizons.
- ☐ Keep credentials, secrets, and unnecessary personal data out of lock diagnostics and evidence records.
- ☐ Separate a coherent local commit from upload, platform processing, listing, and verified public reachability.
- ☐ Report only the narrowest outcome supported by the responsible authority.
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.