Skip to content

Commit ab53ace

Browse files
committed
fix: preserve existing session backups
1 parent 95b3b38 commit ab53ace

3 files changed

Lines changed: 51 additions & 72 deletions

File tree

packages/docker-git-session-sync/src/backup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,13 @@ const runSessionUpload = (
587587
const uploadEntries = [...prepared.uploadEntries, buildReadmeUploadEntry(readmeRepoPath, readmePath)]
588588
logVerbose(verbose, output, `Uploading snapshot to ${backupRepo.fullName}:${context.snapshotRef}`)
589589
const uploadResult = uploadSnapshot(backupRepo, context.snapshotRef, manifest, uploadEntries, ghEnv)
590+
if (!uploadResult.changed) {
591+
output.out(`[session-backup] skipped: no new or changed chat transcripts (${summary.fileCount} files, ${formatBytes(summary.totalBytes)})`)
592+
printGitStatus(output, context.gitStatus)
593+
logVerbose(verbose, output, `[session-backup] No backup repo changes for ${backupRepo.fullName}:${context.snapshotRef}`)
594+
updateUploadComment(context, ghEnv, output, { state: "skipped", message: "No new or changed chat transcripts." })
595+
return 0
596+
}
590597
output.out(`[session-backup] ok: ${context.source.commitSha.slice(0, 12)} (${summary.fileCount} files, ${formatBytes(summary.totalBytes)})`)
591598
printGitStatus(output, context.gitStatus)
592599
logVerbose(verbose, output, `[session-backup] Uploaded snapshot to ${backupRepo.fullName}:${context.snapshotRef}`)

packages/docker-git-session-sync/src/shell.ts

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ export const prepareUploadArtifacts = (
516516
return { uploadEntries, manifestFiles }
517517
}
518518

519-
type GitTreeChange = {
519+
export type GitTreeChange = {
520520
readonly path: string
521521
readonly mode: "100644"
522522
readonly type: "blob"
@@ -533,25 +533,6 @@ const buildFileMapFromTreeEntries = (entries: ReadonlyArray<TreeEntry>): Map<str
533533
return fileMap
534534
}
535535

536-
export const removeSnapshotTreeEntries = (
537-
entries: ReadonlyArray<TreeEntry>,
538-
snapshotRef: string
539-
): ReadonlyArray<TreeEntry> => {
540-
const snapshotPrefix = `${snapshotRef}/`
541-
return entries.filter((entry) => entry.path !== snapshotRef && !entry.path.startsWith(snapshotPrefix))
542-
}
543-
544-
export const buildSnapshotDeleteTreeEntries = (
545-
entries: ReadonlyArray<TreeEntry>,
546-
snapshotRef: string,
547-
desiredPaths: ReadonlySet<string>
548-
): ReadonlyArray<GitTreeChange> => {
549-
const snapshotPrefix = `${snapshotRef}/`
550-
return entries
551-
.filter((entry) => entry.type !== "tree" && entry.path.startsWith(snapshotPrefix) && !desiredPaths.has(entry.path))
552-
.map((entry) => ({ path: entry.path, mode: "100644", type: "blob", sha: null }))
553-
}
554-
555536
const createGitBlob = (repoFullName: string, entry: UploadEntry, ghEnv: GhEnv): string => {
556537
const content = fs.readFileSync(entry.sourcePath)
557538
const result = ensureSuccess(
@@ -641,15 +622,13 @@ const updateGitRef = (repoFullName: string, branch: string, commitSha: string, g
641622
const isRefUpdateConflict = (result: CommandResult): boolean =>
642623
/409|Conflict|Reference update failed|fast[- ]forward/iu.test(`${result.stderr}\n${result.stdout}`)
643624

644-
const buildUploadTreeChanges = (
625+
export const buildUploadTreeChanges = (
645626
repoFullName: string,
646-
snapshotRef: string,
647627
existingEntries: ReadonlyArray<TreeEntry>,
648628
desiredEntries: ReadonlyArray<UploadEntry>,
649629
ghEnv: GhEnv
650630
): ReadonlyArray<GitTreeChange> => {
651631
const existingFileMap = buildFileMapFromTreeEntries(existingEntries)
652-
const desiredPaths = new Set(desiredEntries.map((entry) => entry.repoPath))
653632
const changes: Array<GitTreeChange> = []
654633
for (const entry of desiredEntries) {
655634
if (existingFileMap.get(entry.repoPath)?.sha === entry.blobSha) {
@@ -662,17 +641,27 @@ const buildUploadTreeChanges = (
662641
sha: createGitBlob(repoFullName, entry, ghEnv)
663642
})
664643
}
665-
changes.push(...buildSnapshotDeleteTreeEntries(existingEntries, snapshotRef, desiredPaths))
666644
return changes
667645
}
668646

647+
export const hasChangedUploadEntries = (
648+
existingEntries: ReadonlyArray<TreeEntry>,
649+
desiredEntries: ReadonlyArray<UploadEntry>
650+
): boolean => {
651+
const existingFileMap = buildFileMapFromTreeEntries(existingEntries)
652+
return desiredEntries.some((entry) => existingFileMap.get(entry.repoPath)?.sha !== entry.blobSha)
653+
}
654+
655+
const isContentUploadEntry = (entry: UploadEntry): boolean =>
656+
entry.type !== "readme" && entry.type !== "manifest"
657+
669658
export const uploadSnapshot = (
670659
backupRepo: BackupRepo,
671660
snapshotRef: string,
672661
snapshotManifest: SnapshotManifest,
673662
uploadEntries: ReadonlyArray<UploadEntry>,
674663
ghEnv: GhEnv
675-
): { readonly commitSha: string; readonly manifestPath: string; readonly manifestUrl: string } => {
664+
): { readonly changed: boolean; readonly commitSha: string; readonly manifestPath: string; readonly manifestUrl: string } => {
676665
const uploadRoot = fs.mkdtempSync(path.join(os.tmpdir(), "session-backup-api-"))
677666
const manifestPath = `${snapshotRef}/manifest.json`
678667
const manifestTempPath = path.join(uploadRoot, "manifest.json")
@@ -691,15 +680,23 @@ export const uploadSnapshot = (
691680
if (currentTree.headSha === undefined) {
692681
throw new Error(`failed to resolve ${backupRepo.fullName}@${backupRepo.defaultBranch} head`)
693682
}
683+
if (!hasChangedUploadEntries(currentTree.entries, uploadEntries.filter(isContentUploadEntry))) {
684+
return {
685+
changed: false,
686+
commitSha: currentTree.headSha,
687+
manifestPath,
688+
manifestUrl: buildBlobUrl(backupRepo.fullName, backupRepo.defaultBranch, manifestPath)
689+
}
690+
}
694691
const changes = buildUploadTreeChanges(
695692
backupRepo.fullName,
696-
snapshotRef,
697693
currentTree.entries,
698694
desiredEntries,
699695
ghEnv
700696
)
701697
if (changes.length === 0) {
702698
return {
699+
changed: false,
703700
commitSha: currentTree.headSha,
704701
manifestPath,
705702
manifestUrl: buildBlobUrl(backupRepo.fullName, backupRepo.defaultBranch, manifestPath)
@@ -710,6 +707,7 @@ export const uploadSnapshot = (
710707
const updateResult = updateGitRef(backupRepo.fullName, backupRepo.defaultBranch, commitSha, ghEnv)
711708
if (updateResult.success) {
712709
return {
710+
changed: true,
713711
commitSha,
714712
manifestPath,
715713
manifestUrl: buildBlobUrl(backupRepo.fullName, backupRepo.defaultBranch, manifestPath)

packages/docker-git-session-sync/tests/session-files.test.ts

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import {
1313
import { collectSessionFiles, parseUploadContext, uploadFromContext, type Output } from "../src/backup.js"
1414
import { parseArgs } from "../src/cli.js"
1515
import {
16-
buildSnapshotDeleteTreeEntries,
16+
buildUploadTreeChanges,
1717
gitBlobShaForBuffer,
18-
prepareUploadArtifacts,
19-
removeSnapshotTreeEntries
18+
hasChangedUploadEntries,
19+
prepareUploadArtifacts
2020
} from "../src/shell.js"
21-
import type { TreeEntry } from "../src/types.js"
21+
import type { TreeEntry, UploadEntry } from "../src/types.js"
2222

2323
const output: Output = {
2424
out: () => undefined,
@@ -121,15 +121,21 @@ describe("upload artifacts", () => {
121121
})
122122
})
123123

124-
describe("snapshot tree replacement", () => {
125-
it("removes only files under the exact current snapshot prefix", () => {
124+
describe("snapshot tree updates", () => {
125+
it("keeps stale remote session files untouched", () => {
126126
const entries: ReadonlyArray<TreeEntry> = [
127127
{
128128
path: "org/repo/pr-230/current/.codex/sessions/old.jsonl",
129129
mode: "100644",
130130
type: "blob",
131131
sha: "old"
132132
},
133+
{
134+
path: "org/repo/pr-230/current/.codex/sessions/keep.jsonl",
135+
mode: "100644",
136+
type: "blob",
137+
sha: "keep"
138+
},
133139
{
134140
path: "org/repo/pr-230/current-old/.codex/sessions/keep.jsonl",
135141
mode: "100644",
@@ -149,50 +155,18 @@ describe("snapshot tree replacement", () => {
149155
sha: "keep-other-pr"
150156
}
151157
]
152-
153-
expect(removeSnapshotTreeEntries(entries, "org/repo/pr-230/current").map((entry) => entry.path)).toEqual([
154-
"org/repo/pr-230/current-old/.codex/sessions/keep.jsonl",
155-
"org/repo/pr-230/2026-04-26/manifest.json",
156-
"org/repo/pr-231/current/.codex/sessions/keep.jsonl"
157-
])
158-
})
159-
160-
it("builds delete entries only for stale current snapshot files", () => {
161-
const entries: ReadonlyArray<TreeEntry> = [
158+
const desiredEntries: ReadonlyArray<UploadEntry> = [
162159
{
163-
path: "org/repo/pr-230/current/.codex/sessions/old.jsonl",
164-
mode: "100644",
165-
type: "blob",
166-
sha: "old"
167-
},
168-
{
169-
path: "org/repo/pr-230/current/.codex/sessions/keep.jsonl",
170-
mode: "100644",
171-
type: "blob",
172-
sha: "keep"
173-
},
174-
{
175-
path: "org/repo/pr-230/current-old/.codex/sessions/old.jsonl",
176-
mode: "100644",
177-
type: "blob",
178-
sha: "neighbor"
160+
repoPath: "org/repo/pr-230/current/.codex/sessions/keep.jsonl",
161+
sourcePath: path.join(tmpDir, "unused.jsonl"),
162+
type: "file",
163+
size: 4,
164+
blobSha: "keep"
179165
}
180166
]
181167

182-
expect(
183-
buildSnapshotDeleteTreeEntries(
184-
entries,
185-
"org/repo/pr-230/current",
186-
new Set(["org/repo/pr-230/current/.codex/sessions/keep.jsonl"])
187-
)
188-
).toEqual([
189-
{
190-
path: "org/repo/pr-230/current/.codex/sessions/old.jsonl",
191-
mode: "100644",
192-
type: "blob",
193-
sha: null
194-
}
195-
])
168+
expect(hasChangedUploadEntries(entries, desiredEntries)).toBe(false)
169+
expect(buildUploadTreeChanges("backup-owner/docker-git-sessions", entries, desiredEntries, {})).toEqual([])
196170
})
197171
})
198172

0 commit comments

Comments
 (0)