Saving, when the answers come back out of order

Evidence for issue #123 — settings save immediately; no older answer may speak for a newer edit; no writer may replace a version it never saw. Updated after review round 2.

The bug, before anything was changed

The reproduction is a browser suite that parks every write and lets the test choose the order the answers come back in. Run against unmodified main (ef08e84) it is red in sixteen places — the two halves of the issue, both observed rather than assumed:

a file save that lands after newer typing
  ok  the newer text is still in the editor
  FAIL and still in the remembered draft — an older success cannot clear it
       Expected values to be strictly equal:
  FAIL the file is not reported saved while newer text is unwritten
       Expected "actual" to be strictly unequal to:
  ok  the next save writes the newer text
  FAIL …against the base the older save returned, not the one it was typed on
       Expected values to be strictly equal:
  ok  once the current text is committed it is saved, and the draft is released

saving again while a write is still in flight
  FAIL an explicit save during a write is not dropped — the newer revision follows it
       Expected values to be strictly equal:
  FAIL and it is the latest requested text
       Expected values to be strictly equal:
  FAIL save and close does not report success while its buffer is uncommitted
       Expected values to be strictly equal:
  ok  …and does report success once it is

settings save immediately
  ok  mounting settings saves nothing
  FAIL a change dispatches at once, with no debounce to wait out
       nothing was sent within 448ms of the click
  FAIL one write at a time
       Expected values to be strictly equal:
  FAIL the pending change follows the moment the write settles, with no second delay
       no second write after the first settled
  FAIL and it carries both edits
       Expected values to be strictly equal:

a secret description saves immediately too
  FAIL a description dispatches without waiting
       nothing was sent within 410ms
  ok  and it does not read as saved while the newer text is still unwritten
  FAIL the newer description follows the one that was in flight
       the newer description was never sent
  FAIL and it is the newer text
       Expected values to be strictly equal:

closing settings while a write is out
  FAIL the third change is sent too
       no write for the third change
  FAIL a write already sent finishes after the panel closes
       the outstanding write never settled
  FAIL and it carried the change that was made
       Expected values to be strictly equal:

16 failed

What a save says now

Settings heading showing saving…
A change is sent as it is made — no debounce, no blur, no Apply.
Settings heading showing saved
“Saved” means the version now on screen is the version on disk. If newer text is still unwritten, it does not say this.
Settings heading showing not saved with a Retry button
A refused write keeps the change, says so, and stays on screen — the tick fades, this does not.
Settings heading showing changed elsewhere with Keep mine and Use theirs
Two tabs. The server refuses a write whose base revision has moved on — and so does the check after a lost answer, which is the round-two fix: a read-back that differs is not evidence that our write failed, and never authority to replace a version we never saw.
An app-level bar reporting a settings change that did not save
A failure that arrives after the panel was closed. The save outlives its panel, so the report has to as well.
The Jobs threshold field empty and marked as not a number yet
An emptied field on the way from 17 to something else. It is shown, marked, and not saved as a reset to 0.

The suites

129 checks in the browser, 38 on the server. The last seven sections are round two's.

a file save that lands after newer typing
  ok  the newer text is still in the editor
  ok  and still in the remembered draft — an older success cannot clear it
  ok  the file is not reported saved while newer text is unwritten
  ok  the next save writes the newer text
  ok  …against the base the older save returned, not the one it was typed on
  ok  once the current text is committed it is saved, and the draft is released

saving again while a write is still in flight
  ok  an explicit save during a write is not dropped — the newer revision follows it
  ok  and it is the latest requested text
  ok  save and close does not report success while its buffer is uncommitted
  ok  …and does report success once it is
  ok  the viewer settles instead of reporting itself forever

settings save immediately
  ok  mounting settings saves nothing
  ok  a change dispatches at once, with no debounce to wait out
  ok  one write at a time
  ok  the pending change follows the moment the write settles, with no second delay
  ok  and it carries both edits

a secret description saves immediately too
  ok  a description dispatches without waiting
  ok  and it does not read as saved while the newer text is still unwritten
  ok  the newer description follows the one that was in flight
  ok  and it is the newer text

closing settings while a write is out
  ok  the third change is sent too
  ok  a write already sent finishes after the panel closes
  ok  and it carried the change that was made

reopening the panel
  ok  reopening shows what was saved, not the value it was opened on first time
  ok  and the saved description with it

a settings write that fails
  ok  the change is sent
  ok  the failure is on screen
  ok  and that setting does not claim to be saved
  ok  while the descriptions beside it are unaffected — two resources, saved apart
  ok  a failure does not fade the way the tick does
  ok  Retry sends it again
  ok  carrying the change that failed
  ok  and the failure clears once it lands

a file write that is refused
  ok  the save goes out
  ok  the file is not reported saved
  ok  the conflict is named
  ok  and the text is still there — in the editor and in the kept draft
  ok  overwrite sends it again
  ok  without the precondition it was refused on
  ok  and then it really is saved
  ok  with nothing left unwritten

two save handlers on one keystroke
  ok  the save goes out once
  ok  and the second handler does not make a second round trip say the same thing
  ok  the file is saved either way

a file save that lands after the view is gone
  ok  the save goes out
  ok  the newer buffer survives a save that lands after the view closed
  ok  and it is rebased onto the version that save committed
  ok  reopening the file shows the unsaved text, not what was saved under it
  ok  …and still says it is unsaved
  ok  and saving it writes that text
  ok  against the base the older save returned

a file save that lands while another file is open
  ok  a save finishing for one file does not clear another file's draft
  ok  nor mark the open file saved
  ok  nor touch its text
  ok  the open file saves
  ok  and it carries its own file and its own base, not the other file's

a file write that fails has a way to send it again
  ok  the pane saves the buffer
  ok  the failure is named in the strip
  ok  and a failure that is not a conflict still offers Retry
  ok  which sends it again
  ok  with the same text
  ok  and still with its precondition — Retry is not an overwrite
  ok  and then it reads as saved

a number that is not a number yet
  ok  a complete number is saved as it is typed
  ok  with the value in it
  ok  an emptied field sends nothing
  ok  so the setting keeps the last number that was one
  ok  the box keeps what was typed rather than snapping to 0
  ok  and it is marked as not a number yet
  ok  finishing the edit saves the finished number
  ok  and it is the number that was typed

settings changed somewhere else
  ok  a conflict says which kind of failure it is
  ok  and offers both ways out rather than choosing one
  ok  my value is still on screen while the choice is open
  ok  keeping mine sends it again
  ok  against the revision the server reported, not a blind overwrite
  ok  and it is still my value
  ok  taking theirs shows what is stored
  ok  and does not turn round and save it back
  ok  with the conflict cleared

an answer that never comes
  ok  a settings save has a finite window for an answer
  ok  a request that never answers gives the slot back instead of saying "saving…" forever
  ok  and it is not reported as saved
  ok  a retry asks what the server holds before sending anything again
  ok  and a write that had landed after all is recognised, not repeated
  ok  a lost answer with nothing committed is sent again
  ok  with the value that was owed
  ok  and a precondition, not a forced write

a settings change whose panel is closed before it fails
  ok  a failure that arrives after the panel is closed is reported in the app
  ok  with a way back to the setting and a way to send it again
  ok  and the change is still there to send
  ok  carrying the value that was made before the panel closed
  ok  reopening shows the value still owed to the server, not the older one it would fetch
  ok  and the app-level report stands down while the panel is open
  ok  retrying from the panel sends it once more
  ok  and it lands

a lost answer while somebody else was writing
  ok  a value we never saw is not overwritten to recover our own
  ok  so their change is still on the server
  ok  and it is reported as a conflict, not as saved
  ok  keeping mine is what sends it — a click, not a recovery step
  ok  against the revision the check found

a check that is itself unanswered
  ok  a read-back that never answers gives the slot back too
  ok  and stays retryable
  ok  a later edit is not trapped behind it
  ok  and it carries the newer value

reopening settings while a write is still out
  ok  the value being written is what the panel comes back to
  ok  the edit that followed it goes out
  ok  carrying both fields — the one still in flight is not reverted
  ok  and what ends up stored is the value that was made, not an older one

a Save saves what was asked for, not what is typed after it
  ok  the queued save goes out
  ok  and it carries the version that was asked for
  ok  text typed after that Save stays dirty rather than being committed by it
  ok  with its draft intact
  ok  and still on screen

what save and close is told when the buffer moves on
  ok  a write that succeeded for older text is not a yes to "is the buffer saved?"
  ok  the newer text is still unsaved, and says so
  ok  with its draft kept

save and close, and then changing your mind
  ok  save and close sends the buffer
  ok  an older success does not reverse Keep editing
  ok  the newer text is still the buffer
  ok  and the file does not read as saved
  ok  Keep editing keeps the file open even when the save it interrupted succeeds
  ok  save and close sends the newer buffer
  ok  which is the text on screen
  ok  and then it does close
  ok  with nothing left unsaved

a derived update that fails after the save has landed
  ok  a derived failure that arrives after the save reaches the open panel
  ok  without sending the settings again to find out
  ok  and the save itself still reads as saved — they are different outcomes

save-races: ok

Server

PASS  a first save needs no revision — there is nothing to replace  status 200
PASS  and answers with the revision it committed
PASS  reading it back agrees, revision included  {"rev":"294a59d9af46c43e"}
PASS  the file it was written to is whole JSON, not a fragment
PASS  and nothing is left behind beside it  am-config.json,crons.json,home,operations.jsonl,state,workspaces
PASS  replacing settings that exist without saying which version is refused  status 409 "base-required"
PASS  the first of two tabs commits
PASS  the second, still holding the old revision, is refused  status 409 "stale"
PASS  and is told what is actually stored, so it can reconcile  {"enabled":true,"space":"someone/newer","visibility":"public"}
PASS  the first tab's change is still there — nothing was silently erased  {"space":"someone/newer","usd":12}
PASS  and the refused writer succeeds once it replaces the version it was told about
PASS  a failed write is answered as a failure, not as ok  status 500 {"error":"could not save settings — EACCES: permission denied, open '/tmp/am-settings-write-YmAciG/.
PASS  with something the operator can read  could not save settings — EACCES: permission denied, open '/tmp/am-settings-write-YmAciG/.am-config.json.am-tmp'
PASS  the settings that were there are whole and untouched
PASS  and no half-written file is left beside them  am-config.json,crons.json,home,operations.jsonl,state,workspaces
PASS  a save that said ok is still there when the settings are read again  answered undefined, reads back 99
PASS  a damaged settings file is reported on the read, not silently defaulted over  "am-config.json is not valid JSON — Expected double-quoted property name in JSON at position 52 (line 1 column 53)"
PASS  and an ordinary edit does not get to overwrite it  status 409 "unreadable"
PASS  the bytes that were there are still there
PASS  descriptions save the same way
PASS  and land as whole JSON
PASS  a description written against a revision that has moved on is refused
PASS  the probe variable is detected, so the generated skill has something to describe  24 detected
PASS  a burst of saves leaves the generated skill on the last one
PASS  and not on one it overtook
PASS  with nothing reported as pending or failed once it has converged  {"pending":false,"error":null,"at":1788980263377}
PASS  an array is not an object of descriptions, and is refused  status 400 "notes must be an object of name → description"
PASS  and neither is a description that is not text
PASS  the descriptions that were there are untouched
PASS  so the file still reads, and ordinary saves still work  null
PASS  proved by making one
PASS  a settings body that is not an object is refused rather than normalized to defaults  status 400
PASS  the derived outcome can be read without sending the settings again  {"pending":false,"error":null,"at":1788980263392}
PASS  a settings save still succeeds when the derived update cannot be written  status 200
PASS  and the file it was asked to save really is saved
PASS  but the derived failure is reported, not swallowed into a fully applied state  {"pending":false,"error":"EACCES: permission denied, open '/tmp/am-settings-write-YmAciG/workspaces/skills/environment.md'","at":1788980263398}
PASS  and it clears once a later save gets the derived update through  {"pending":false,"error":null,"at":1788980263405}
PASS  with the skill on the newest description

38 passed, 0 failed

Mutation checks

Every guard was reverted on its own; each line is what the suites then said.

Round two

Round one

Generated for the PR against huggingface/agent-manager. Screenshots are rendered from the components themselves with the network stubbed; no live settings, secrets or workspaces were read or written. The server suite now supplies its own probe variable, so it says the same thing on a machine with no credentials in the environment.