Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c6de6a8
feat: allow snapshot config to accept positive integer for retention …
ariane-emory Jan 26, 2026
96f59e1
feat: allow snapshot config to accept 0 to disable snapshots
ariane-emory Jan 26, 2026
68a9de7
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Jan 26, 2026
565b83a
Implement configurable snapshot lifespan cleanup logic
ariane-emory Jan 27, 2026
c5d8b80
Refactor snapshot config tests to verify retention calculation
ariane-emory Jan 27, 2026
3cf4e02
Fix TypeScript type errors in snapshot config tests
ariane-emory Jan 27, 2026
78cf530
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Jan 27, 2026
aca58d5
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Jan 29, 2026
45cf6f7
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Jan 29, 2026
d5d17c9
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Jan 30, 2026
2a30f41
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 1, 2026
756a969
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 2, 2026
0695ff5
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 3, 2026
851422a
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 4, 2026
71dcec0
Merge dev into feat/configurable-snapshot-lifespan
ariane-emory Feb 4, 2026
44d80d1
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 6, 2026
b773ac9
Merge dev into feat/configurable-snapshot-lifespan
ariane-emory Feb 9, 2026
17ba66f
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 10, 2026
aba59c2
Merge dev into feat/configurable-snapshot-lifespan
ariane-emory Feb 11, 2026
7587d2e
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 12, 2026
80c20d3
Merge dev into feat/configurable-snapshot-lifespan
ariane-emory Feb 13, 2026
a6a2ca0
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 14, 2026
a9c314b
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 15, 2026
03d2776
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 15, 2026
70c77de
Merge branch 'dev' into feat/configurable-snapshot-lifespan
ariane-emory Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,7 @@ export namespace Config {
})
.optional(),
plugin: z.string().array().optional(),
snapshot: z.boolean().optional(),
snapshot: z.union([z.boolean(), z.number().int().nonnegative()]).optional(),
share: z
.enum(["manual", "auto", "disabled"])
.optional()
Expand Down
47 changes: 26 additions & 21 deletions packages/opencode/src/snapshot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { Scheduler } from "../scheduler"
export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
const hour = 60 * 60 * 1000
const prune = "7.days"

export function init() {
Scheduler.register({
Expand All @@ -26,32 +25,38 @@ export namespace Snapshot {
export async function cleanup() {
if (Instance.project.vcs !== "git" || Flag.OPENCODE_CLIENT === "acp") return
const cfg = await Config.get()
if (cfg.snapshot === false) return
const git = gitdir()
const exists = await fs
.stat(git)
.then(() => true)
.catch(() => false)
if (!exists) return
const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} gc --prune=${prune}`
.quiet()
.cwd(Instance.directory)
.nothrow()
if (result.exitCode !== 0) {
log.warn("cleanup failed", {
exitCode: result.exitCode,
stderr: result.stderr.toString(),
stdout: result.stdout.toString(),
})
return
if (cfg.snapshot === false || cfg.snapshot === 0 || cfg.snapshot === undefined) return
const retentionDays: number = cfg.snapshot === true ? 7 : cfg.snapshot!
const snapshotDir = gitdir()
const parentDir = path.dirname(snapshotDir)
try {
const entries = await fs.readdir(parentDir, { withFileTypes: true })
let deletedCount = 0
for (const entry of entries) {
if (!entry.isDirectory()) continue
const projectDir = path.join(parentDir, entry.name)
const stats = await fs.stat(projectDir)
const ageMs = Date.now() - stats.mtimeMs
const ageDays = ageMs / (24 * 60 * 60 * 1000)
if (ageDays > retentionDays) {
await fs.rm(projectDir, { recursive: true, force: true })
deletedCount++
log.info("deleted old snapshot directory", {
project: entry.name,
ageDays: Math.floor(ageDays),
})
}
}
log.info("cleanup", { retentionDays, deletedCount })
} catch (error) {
log.warn("cleanup failed", { error: (error as Error).message })
}
log.info("cleanup", { prune })
}

export async function track() {
if (Instance.project.vcs !== "git" || Flag.OPENCODE_CLIENT === "acp") return
const cfg = await Config.get()
if (cfg.snapshot === false) return
if (cfg.snapshot === false || cfg.snapshot === 0) return
const git = gitdir()
if (await fs.mkdir(git, { recursive: true })) {
await $`git init`
Expand Down
14 changes: 14 additions & 0 deletions packages/opencode/test/snapshot/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { test, expect } from "bun:test"
import { $ } from "bun"
import { Snapshot } from "../../src/snapshot"
import { Instance } from "../../src/project/instance"
import { Config } from "../../src/config/config"
import { tmpdir } from "../fixture/fixture"

async function bootstrap() {
Expand Down Expand Up @@ -1038,3 +1039,16 @@ test("diffFull with whitespace changes", async () => {
},
})
})

test("snapshot config with boolean true uses default 7-day retention", async () => {
const cfg = { snapshot: true as true | number }
const retentionDays = cfg.snapshot === true ? 7 : cfg.snapshot
expect(retentionDays).toBe(7)
})

test("snapshot config with positive integer uses specified retention", async () => {
const cfg = { snapshot: 3 as true | number }
const retentionDays = cfg.snapshot === true ? 7 : cfg.snapshot
expect(retentionDays).toBe(3)
})

2 changes: 1 addition & 1 deletion packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1730,7 +1730,7 @@ export type Config = {
ignore?: Array<string>
}
plugin?: Array<string>
snapshot?: boolean
snapshot?: boolean | number
/**
* Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing
*/
Expand Down