275780299a
A tool call is bounded by a transport deadline; when it fires, boundHandler
answers the client and lets the handler keep running. agents.AtomicWriteFile
takes no context, so a handler that had already reached its rename committed to
disk anyway — and the caller was told only "the work may still complete in the
background, so treat any side effect as unknown".
That single message covered two opposite outcomes. A client reading it as
"nothing happened" retried, or fell back to its own editor, and applied the same
logical change twice.
Split the two states and make both observable:
- Every disk commit registers in a durable ledger BEFORE the write and is
stamped terminal immediately after it, so a receipt exists for any mutation
that is still reachable.
- commitFileMutation adds a cancellation gate between registration and the
write. Stopping there is a guarantee that nothing was written, not a guess;
the remaining window is one atomic rename wide and is covered by an
in_flight receipt.
- The abandoned-call error now reports what actually landed — committed with
path and new_sha, not_applied, or genuinely unknown — plus a machine-readable
mutation_commit={...} tail. Read tools keep the original wording.
- Edit responses carry disk_status and graph_status as independent fields:
bytes reaching disk and the graph catching up are different questions with
different failure modes.
- mutation_status (facade: change/receipt) queries a receipt by id,
mutation_id, or path for 30 minutes, refreshing a still-pending graph state
rather than answering from a stale snapshot.
- edit_file / write_file / edit_symbol take an optional mutation_id
idempotency key, mirroring batch_edit's transaction_id: the same key with the
identical edit replays the original result instead of writing twice, and a
different edit under the same key is refused.
rename_symbol and the batch symbol/file edits register in the same ledger; a
coordinated rename now stops at its first cancelled write instead of attempting
every remaining file.
The agent preset's tools/list ceiling is re-based 28200 -> 28400 for the
mutation_id parameter on the two edit tools (148 bytes measured); the preset
itself did not grow.
Fixes #548