fix(revisions): leave revision state unchanged when a write fails #28
No reviewers
Labels
No labels
area/client
area/revisions
bug
clarity/ambiguous
clarity/clear
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
size/L
size/M
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
basepeak-ai/workspace-provider!28
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/revisions-on-failure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of basepeak-ai/bpai#1220. Follows #27.
With revisions on, a write bumped the revision counter and saved the old content as a revision before reading the new body. If the body then failed part-way, nothing was stored but the counter had moved. A retry with the old
latestRevisionIDgot a spurious conflict, and history held a duplicate. Streaming makes a failure part-way the normal way for a write to fail.Design
Per-file lock (in-process). The key is provider + workspace + file name. For the directory provider the name is normalised, so
F.txt,/F.txtand./F.txtshare one lock.WriteFile,DeleteFileandDeleteRevisiontake it.A revisioned write of revision N = CurrentID+1, under the lock:
PendingID == CurrentID+1→CurrentID = PendingID). Then run the conflict check.PendingID = N;CurrentIDstays unchanged.CurrentID = NandPendingIDcleared. This is retried; if it still fails,RevisionNotRecordedErrornames the new latest revision to use as the next base.What happens when something fails:
ConflictError, nothing touched (the regex go-gptscript parses still matches)DeleteRevisionrecords the predecessor, and a writer with the old base gets a conflictDeleteFileOther rules:
CreateRevision=falsenever touch the revision store, as on main. Every knowledge connector writes that way.GetRevisionreturns NotFound above CurrentID;DeleteRevisionis a no-op there, as main is for missing revisions.DeleteFiledeletes revisions 0 through CurrentID+1.StatFileskips the ranged mimetype read only for an object known to be empty.Limits are documented in
filelock.go:Review
Three fresh-context review rounds, each with a correctness reviewer and a silent-failure hunter. Every reported data-loss sequence was reproduced and is now a regression test:
DeleteFilebecoming a wrong revision;"json"revision IDs deleting the index;F.0survivingDeleteFile;DeleteFileretry not finishing the cleanup;An 8-goroutine × 15-round conditional-write stress test showed no base winning twice. Each fix was mutation-checked: reverting it fails at least one test.
Checks
go vet,golangci-lint(0 issues),go mod tidy(no diff).go test -count=1 -race ./...: 92 passed, 59 credential-gated skipped.976fb9a; the three commits since change only an error message, a doc comment and tests.Requests per write on S3
Follow-ups, out of scope (all pre-existing)
CreateRevision=false.Rmleaves revision files behind.b7949253b84e895057ca