From 630a0dc664b9bfea871ed13bb19409b589503315 Mon Sep 17 00:00:00 2001 From: agentrelaybot Date: Sun, 4 Oct 2026 09:08:11 -0700 Subject: [PATCH 1/2] test(mount): prove complete snapshot repull preserves files --- .../completed/2026-10/traj_t8ngcsdcvvc7.json | 57 ++++++ .../completed/2026-10/traj_t8ngcsdcvvc7.md | 24 +++ .trajectories/index.json | 11 +- internal/mountsync/syncer_test.go | 184 ++++++++++++++++++ 4 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 .trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json create mode 100644 .trajectories/completed/2026-10/traj_t8ngcsdcvvc7.md diff --git a/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json b/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json new file mode 100644 index 00000000..9688f931 --- /dev/null +++ b/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json @@ -0,0 +1,57 @@ +{ + "id": "traj_t8ngcsdcvvc7", + "version": 1, + "task": { + "title": "Prove bootstrap-complete full re-pull preserves snapshot files after agent edit", + "source": { + "system": "plain", + "id": "Agent37 GA mount no-deletion proof" + } + }, + "status": "completed", + "startedAt": "2026-10-04T15:50:26.132Z", + "completedAt": "2026-10-04T16:07:54.908Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-10-04T16:03:18.492Z" + } + ], + "chapters": [ + { + "id": "chap_pyckl5knsrzt", + "title": "Work", + "agentName": "default", + "startedAt": "2026-10-04T16:03:18.492Z", + "endedAt": "2026-10-04T16:07:54.908Z", + "events": [ + { + "ts": 1791129798492, + "type": "decision", + "content": "Use an ExportFiles-only fake for the snapshot-backed mount so the regression reaches the post-bootstrap format=json branch instead of the GitHub tar-seed interface; pair the complete export assertion with two advancing incomplete snapshots to prove the tombstone delete consequence.: Use an ExportFiles-only fake for the snapshot-backed mount so the regression reaches the post-bootstrap format=json branch instead of the GitHub tar-seed interface; pair the complete export assertion with two advancing incomplete snapshots to prove the tombstone delete consequence.", + "raw": { + "question": "Use an ExportFiles-only fake for the snapshot-backed mount so the regression reaches the post-bootstrap format=json branch instead of the GitHub tar-seed interface; pair the complete export assertion with two advancing incomplete snapshots to prove the tombstone delete consequence.", + "chosen": "Use an ExportFiles-only fake for the snapshot-backed mount so the regression reaches the post-bootstrap format=json branch instead of the GitHub tar-seed interface; pair the complete export assertion with two advancing incomplete snapshots to prove the tombstone delete consequence.", + "alternatives": [], + "reasoning": "" + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Added the exact Agent37 post-bootstrap snapshot regression: an agent edit followed by one ExportFiles JSON full re-pull preserves all untouched files and creates no delete tombstones; a two-observation incomplete-export control proves the deletion consequence.", + "approach": "Standard approach", + "confidence": 0.95 + }, + "commits": [], + "filesChanged": [], + "projectId": "AgentWorkforce/relayfile", + "tags": [], + "_trace": { + "startRef": "8d34a161c3e306a68b91958adb8b7ca3959aac4c", + "endRef": "8d34a161c3e306a68b91958adb8b7ca3959aac4c" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.md b/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.md new file mode 100644 index 00000000..b72ae9b7 --- /dev/null +++ b/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.md @@ -0,0 +1,24 @@ +# Trajectory: Prove bootstrap-complete full re-pull preserves snapshot files after agent edit + +> **Status:** ✅ Completed +> **Task:** Agent37 GA mount no-deletion proof +> **Confidence:** 95% +> **Started:** October 4, 2026 at 08:50 AM +> **Completed:** October 4, 2026 at 09:07 AM + +--- + +## Summary + +Added the exact Agent37 post-bootstrap snapshot regression: an agent edit followed by one ExportFiles JSON full re-pull preserves all untouched files and creates no delete tombstones; a two-observation incomplete-export control proves the deletion consequence. + +**Approach:** Standard approach + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Use an ExportFiles-only fake for the snapshot-backed mount so the regression reaches the post-bootstrap format=json branch instead of the GitHub tar-seed interface; pair the complete export assertion with two advancing incomplete snapshots to prove the tombstone delete consequence.: Use an ExportFiles-only fake for the snapshot-backed mount so the regression reaches the post-bootstrap format=json branch instead of the GitHub tar-seed interface; pair the complete export assertion with two advancing incomplete snapshots to prove the tombstone delete consequence. diff --git a/.trajectories/index.json b/.trajectories/index.json index e8431851..164251d8 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-10-03T16:08:45.366Z", + "lastUpdated": "2026-10-04T16:07:55.245Z", "trajectories": { "traj_4pvrlmqfnzng": { "title": "Review PR #278 in AgentWorkforce/relayfile", @@ -456,6 +456,13 @@ "startedAt": "2026-10-03T15:42:22.196Z", "completedAt": "2026-10-03T16:08:45.325Z", "path": ".trajectories/completed/2026-10/traj_1p4an8ifdgfv.json" + }, + "traj_t8ngcsdcvvc7": { + "title": "Prove bootstrap-complete full re-pull preserves snapshot files after agent edit", + "status": "completed", + "startedAt": "2026-10-04T15:50:26.132Z", + "completedAt": "2026-10-04T16:07:54.908Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/.worktrees/agent37-ga-mount-proof/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json" } } -} +} \ No newline at end of file diff --git a/internal/mountsync/syncer_test.go b/internal/mountsync/syncer_test.go index 6243c42d..2ccb1207 100644 --- a/internal/mountsync/syncer_test.go +++ b/internal/mountsync/syncer_test.go @@ -2232,6 +2232,161 @@ func TestReconcileUsesBoundedTreeForFreshScopedInitialPull(t *testing.T) { } } +func TestBootstrapCompleteFullRepullPreservesUntouchedSnapshotFilesAfterAgentEdit(t *testing.T) { + const ( + workspaceID = "ws_complete_snapshot_export" + contentsRoot = "/github/repos/AgentWorkforce/cloud/contents" + headSHA = "head123" + editedRel = "src/app.ts" + ) + remotePath := func(rel string) string { + return contentsRoot + "/" + rel + "@" + headSHA + ".json" + } + initial := map[string]string{ + "README.md": "# Cloud\n", + "docs/guide.md": "# Guide\n", + editedRel: "export const answer = 41;\n", + "src/config.ts": "export const enabled = true;\n", + } + const agentEdit = "export const answer = 42;\n" + + newMountedSnapshot := func(t *testing.T) (*Syncer, *fakeJSONExportClient, string) { + t.Helper() + base := &fakeClient{ + files: make(map[string]RemoteFile, len(initial)), + revisionCounter: len(initial), + } + client := &fakeJSONExportClient{fakeClient: base} + localDir := t.TempDir() + syncer, err := NewSyncer(client, SyncerOptions{ + WorkspaceID: workspaceID, + RemoteRoot: contentsRoot, + LocalRoot: localDir, + WebSocket: boolPtr(false), + FullPullEvery: -1, + }) + if err != nil { + t.Fatalf("NewSyncer: %v", err) + } + syncer.githubWorkingTree.HeadSHA = headSHA + syncer.state.GithubWorkingTreeHeadSHA = headSHA + syncer.state.BootstrapComplete = true + syncer.state.LastAppliedRevision = "rev_4" + + rels := make([]string, 0, len(initial)) + for rel := range initial { + rels = append(rels, rel) + } + sort.Strings(rels) + for index, rel := range rels { + content := initial[rel] + path := remotePath(rel) + revision := fmt.Sprintf("rev_%d", index+1) + base.files[path] = RemoteFile{ + Path: path, + Revision: revision, + ContentType: detectContentType(rel), + Content: content, + ContentHash: hashString(content), + } + localPath := filepath.Join(localDir, filepath.FromSlash(rel)) + if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil { + t.Fatalf("mkdir %s: %v", rel, err) + } + if err := os.WriteFile(localPath, []byte(content), 0o644); err != nil { + t.Fatalf("seed %s: %v", rel, err) + } + syncer.state.Files[path] = trackedFile{ + Revision: revision, + ContentType: detectContentType(rel), + Hash: hashString(content), + } + } + if err := syncer.saveState(); err != nil { + t.Fatalf("persist bootstrap-complete snapshot state: %v", err) + } + + editedPath := filepath.Join(localDir, filepath.FromSlash(editedRel)) + if err := os.WriteFile(editedPath, []byte(agentEdit), 0o644); err != nil { + t.Fatalf("write agent edit: %v", err) + } + if err := syncer.HandleLocalChange(context.Background(), editedRel, fsnotify.Write); err != nil { + t.Fatalf("push agent edit: %v", err) + } + if got := base.files[remotePath(editedRel)].Content; got != agentEdit { + t.Fatalf("agent edit was not written through before re-pull: got %q", got) + } + return syncer, client, localDir + } + + t.Run("complete format=json export preserves every untouched file", func(t *testing.T) { + syncer, client, localDir := newMountedSnapshot(t) + + if err := syncer.pullRemoteFull(context.Background(), nil, bootstrapProgress{}); err != nil { + t.Fatalf("full re-pull: %v", err) + } + + if client.exportCalls != 1 || len(client.exportPaths) != 1 || client.exportPaths[0] != contentsRoot { + t.Fatalf("full re-pull did not use one ExportFiles format=json snapshot for %s: calls=%d paths=%v", contentsRoot, client.exportCalls, client.exportPaths) + } + if client.listTreeCalls != 0 { + t.Fatalf("full re-pull unexpectedly used tree fallback: calls=%d", client.listTreeCalls) + } + if got := len(syncer.state.Files); got != len(initial) { + t.Fatalf("tracked files after complete export = %d, want %d", got, len(initial)) + } + for rel, content := range initial { + want := content + if rel == editedRel { + want = agentEdit + } + localPath := filepath.Join(localDir, filepath.FromSlash(rel)) + assertLocalFileContent(t, localPath, want) + if _, err := os.Stat(syncer.tombstoneFile(remotePath(rel))); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("complete export created a delete tombstone for %s: %v", rel, err) + } + } + }) + + t.Run("incomplete format=json export would delete an omitted untouched file", func(t *testing.T) { + syncer, client, localDir := newMountedSnapshot(t) + omittedRel := "docs/guide.md" + omittedRemote := remotePath(omittedRel) + client.omitPaths = map[string]struct{}{omittedRemote: {}} + + advanceExportRevision := func(revision string) { + readmeRemote := remotePath("README.md") + readme := client.files[readmeRemote] + readme.Revision = revision + client.files[readmeRemote] = readme + } + advanceExportRevision("rev_6") + if err := syncer.pullRemoteFull(context.Background(), nil, bootstrapProgress{}); err != nil { + t.Fatalf("first incomplete full re-pull: %v", err) + } + omittedLocal := filepath.Join(localDir, filepath.FromSlash(omittedRel)) + if _, err := os.Stat(omittedLocal); err != nil { + t.Fatalf("first incomplete observation must retain %s pending confirmation: %v", omittedRel, err) + } + pending, err := syncer.loadTombstone(omittedRemote) + if err != nil || pending == nil || pending.Attempts != 1 { + t.Fatalf("first incomplete export did not record one pending delete observation: tombstone=%+v err=%v", pending, err) + } + + advanceExportRevision("rev_7") + if err := syncer.pullRemoteFull(context.Background(), nil, bootstrapProgress{}); err != nil { + t.Fatalf("second incomplete full re-pull: %v", err) + } + if _, tracked := syncer.state.Files[omittedRemote]; tracked { + t.Fatalf("omitted snapshot file remained tracked after confirmed incomplete exports") + } + if _, err := os.Stat(omittedLocal); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("two advancing incomplete exports should demonstrate deletion of omitted %s; stat err=%v", omittedRel, err) + } + assertLocalFileContent(t, filepath.Join(localDir, filepath.FromSlash(editedRel)), agentEdit) + }) +} + func TestReconcileSkipsAtomicExportForWorkspaceRoot(t *testing.T) { base := &fakeClient{ files: map[string]RemoteFile{ @@ -10654,6 +10809,35 @@ type fakeExportClient struct { exportRelease <-chan struct{} } +// fakeJSONExportClient implements the atomic ExportFiles path without also +// implementing the GitHub tar-seed interface. That lets post-bootstrap tests +// exercise the same /fs/export?format=json dispatch used by published mounts. +type fakeJSONExportClient struct { + *fakeClient + exportCalls int + exportPaths []string + omitPaths map[string]struct{} +} + +func (c *fakeJSONExportClient) ExportFiles(_ context.Context, _ string, path string) ([]RemoteFile, error) { + c.exportCalls++ + base := normalizeRemotePath(path) + c.exportPaths = append(c.exportPaths, base) + files := make([]RemoteFile, 0, len(c.files)) + for remotePath, file := range c.files { + remotePath = normalizeRemotePath(remotePath) + if !isUnderRemoteRoot(base, remotePath) { + continue + } + if _, omitted := c.omitPaths[remotePath]; omitted { + continue + } + files = append(files, file) + } + sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path }) + return files, nil +} + type blockingGithubTarClient struct { *fakeExportClient tarStarted chan struct{} From 14059ed6ba4000355b2ce4470c5d044150b5aa62 Mon Sep 17 00:00:00 2001 From: agentrelaybot Date: Sun, 4 Oct 2026 09:33:30 -0700 Subject: [PATCH 2/2] chore(trail): record mount proof provenance --- .../completed/2026-10/traj_t8ngcsdcvvc7.json | 15 +++++++++++---- .trajectories/index.json | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json b/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json index 9688f931..2227dd03 100644 --- a/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json +++ b/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json @@ -46,12 +46,19 @@ "approach": "Standard approach", "confidence": 0.95 }, - "commits": [], - "filesChanged": [], + "commits": [ + "630a0dc6" + ], + "filesChanged": [ + ".trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json", + ".trajectories/completed/2026-10/traj_t8ngcsdcvvc7.md", + ".trajectories/index.json", + "internal/mountsync/syncer_test.go" + ], "projectId": "AgentWorkforce/relayfile", "tags": [], "_trace": { "startRef": "8d34a161c3e306a68b91958adb8b7ca3959aac4c", - "endRef": "8d34a161c3e306a68b91958adb8b7ca3959aac4c" + "endRef": "630a0dc664b9bfea871ed13bb19409b589503315" } -} \ No newline at end of file +} diff --git a/.trajectories/index.json b/.trajectories/index.json index 164251d8..7228ae75 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -462,7 +462,7 @@ "status": "completed", "startedAt": "2026-10-04T15:50:26.132Z", "completedAt": "2026-10-04T16:07:54.908Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/.worktrees/agent37-ga-mount-proof/.trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json" + "path": ".trajectories/completed/2026-10/traj_t8ngcsdcvvc7.json" } } -} \ No newline at end of file +}