Concurrency and soft-locks

~5 min read

Concurrency and soft-locks

What happens when two agents (or an agent and you) touch the same note at the same time.

The model: no explicit version tokens

Lettuce does not use version tokens or optimistic-concurrency hashes on writes. There is no base_sha256 parameter, and no “only apply if the file still matches” precondition. Conflict-safety comes from three concrete mechanisms instead:

  1. Exactly-once matching on apply_edits. Each find string must match exactly once. If the file drifted out from under you — an anchor went missing or now appears twice — the edit fails loudly and the whole call rolls back, rather than writing to the wrong place.

  2. Soft-locks on the editor. A note you’re actively editing is flagged; agent writes are warned or (for full replaces) rejected so they don’t clobber your unsaved buffer.

  3. Proposal staleness re-checks. A queued proposal is re-validated against the live file at accept time; if the target drifted, accepting fails with proposal-stale instead of applying blindly.

Agent A: read_note         → content X (anchor "## Notes" present)
Agent A: thinks for 30s
Agent B: apply_edits        → rewrites that section, anchor text changes
Agent A: apply_edits with find="## Notes\n\nold body"  → ERROR: find not found
Agent A: re-reads, rebuilds find strings, retries → OK

How agents stay conflict-safe

Use apply_edits for modifications: the exactly-once-match rule is itself the guard. Pick find strings specific enough that they can only match the version of the file you read; if someone changed that region first, your edit fails loudly and you re-read and retry.

write_note is a full-content replace, semantically “I want this to be the new content regardless”. It has no anchoring, so it’s the easiest way to clobber a concurrent change — prefer apply_edits for modifications, and use mode: create to guard against accidental overwrite (it fails with note-exists if the file already exists).

What can shift your anchors

So a “no-op” save can still move text around. Agents should expect this and re-read + retry when an apply_edits find stops matching, rather than assuming the world is malicious.

Soft locks for the editor

When you’re actively editing a note in Lettuce, the file is soft-locked for as long as it’s focused in the editor. Any agent write that targets a locked note — apply_edits, write_note, patch_section, or complete_directive — fails with a hard error (kind: target-is-being-edited) rather than touching your open buffer. There’s no “advisory” path: a focused note is off-limits to direct writes.

The agent’s options: wait and retry once you blur or close the note, or run under a propose-only key, where the write is queued as a proposal for you to accept later (so it never contends with your buffer). The lock is released the moment you blur or close the note.

Locking semantics in detail

(The “propose-only key” column is any write tool — apply_edits / write_note / patch_section — issued under a propose-only key, which queues a proposal instead of committing, so the editor lock never applies.)

State write key (apply_edits / write_note / patch_section) propose-only key
Note focused in the editor target-is-being-edited ✅ proposed
Note not open (or QuickLook only) ✅ applies ✅ proposed

Multi-agent contention

Two agents editing the same note:

Multi-vault contention

Each vault has its own lock domain. Agents working on different vaults never block each other. The server is fully async and can handle many simultaneous tool calls.

Practical recipes

Retry loop

for attempt in range(3):
    note = client.tools.read_note(path=path)
    # Build `find` strings from the content you just read, so they only
    # match this version of the file.
    edits = build_edits(note.content)
    try:
        client.tools.apply_edits(path=path, edits=edits)
        break
    except EditNotUnique:
        # Someone changed the region first; the anchor no longer matches.
        # Re-read and rebuild on the next loop.
        continue
else:
    raise RuntimeError(f"Could not apply edits after 3 retries")

Check before clobber

try:
    client.tools.write_note(
        path=path,
        content=...,
        mode="create"        # fails if the file already exists
    )
except NoteExists:           # kind: note-exists
    # someone (or another agent) got there first; use apply_edits
    ...