-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Marvis [T3 Code] Add checkpoint snapshot pruning with retention policy #3655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import * as Layer from "effect/Layer"; | |
| import type { CheckpointStoreError } from "./Errors.ts"; | ||
| import type { VcsCheckpointOps } from "../vcs/VcsDriver.ts"; | ||
| import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | ||
| import { CHECKPOINT_REFS_PREFIX } from "./Utils.ts"; | ||
|
|
||
| export interface CaptureCheckpointInput { | ||
| readonly cwd: string; | ||
|
|
@@ -46,6 +47,10 @@ export interface DeleteCheckpointRefsInput { | |
| readonly checkpointRefs: ReadonlyArray<CheckpointRef>; | ||
| } | ||
|
|
||
| export interface PruneSnapshotsResult { | ||
| readonly deleted: number; | ||
| } | ||
|
|
||
| /** Service tag for checkpoint persistence and restore operations. */ | ||
| export class CheckpointStore extends Context.Service< | ||
| CheckpointStore, | ||
|
|
@@ -93,6 +98,16 @@ export class CheckpointStore extends Context.Service< | |
| readonly deleteCheckpointRefs: ( | ||
| input: DeleteCheckpointRefsInput, | ||
| ) => Effect.Effect<void, CheckpointStoreError>; | ||
|
|
||
| /** | ||
| * Prune checkpoint snapshots, keeping only the 3 most recent per session. | ||
| * | ||
| * Lists all checkpoint refs, groups by session, and deletes snapshots | ||
| * beyond the 3 most recent per session. Returns the count of deleted snapshots. | ||
| */ | ||
| readonly pruneSnapshots: ( | ||
| cwd: string, | ||
| ) => Effect.Effect<PruneSnapshotsResult, CheckpointStoreError>; | ||
| } | ||
| >()("t3/checkpointing/CheckpointStore") {} | ||
|
|
||
|
|
@@ -157,13 +172,64 @@ export const make = Effect.gen(function* () { | |
| return yield* checkpoints.deleteCheckpointRefs(input); | ||
| }); | ||
|
|
||
| const pruneSnapshots: CheckpointStore["Service"]["pruneSnapshots"] = Effect.fn( | ||
| "pruneSnapshots", | ||
| )(function* (cwd) { | ||
| const checkpoints = yield* resolveCheckpoints("CheckpointStore.pruneSnapshots", cwd); | ||
| const allRefs = yield* checkpoints.listCheckpointRefs({ cwd }); | ||
|
|
||
| if (allRefs.length === 0) { | ||
| return { deleted: 0 }; | ||
| } | ||
|
|
||
| // Parse each ref: refs/t3/checkpoints/{threadId}/turn/{turnCount} | ||
| const parsed = allRefs | ||
| .map((ref) => { | ||
| const refStr = String(ref); | ||
| const prefix = `${CHECKPOINT_REFS_PREFIX}/`; | ||
| const afterPrefix = refStr.slice(refStr.indexOf(prefix) + prefix.length); | ||
| const parts = afterPrefix.split("/turn/"); | ||
| if (parts.length !== 2) return null; | ||
| const sessionKey = parts[0]; | ||
| const turnCount = parseInt(parts[1], 10); | ||
| if (isNaN(turnCount)) return null; | ||
| return { ref, sessionKey, turnCount }; | ||
| }) | ||
| .filter((p): p is NonNullable<typeof p> => p !== null); | ||
|
|
||
| // Group by session, keep top 3 per session | ||
| const bySession = new Map<string, Array<{ ref: CheckpointRef; turnCount: number }>>(); | ||
| for (const p of parsed) { | ||
| const existing = bySession.get(p.sessionKey) ?? []; | ||
| existing.push({ ref: p.ref, turnCount: p.turnCount }); | ||
| bySession.set(p.sessionKey, existing); | ||
| } | ||
|
|
||
| const toPrune: CheckpointRef[] = []; | ||
| for (const [, entries] of bySession) { | ||
| entries.sort((a, b) => b.turnCount - a.turnCount); | ||
| const exceed = entries.slice(3); | ||
| for (const e of exceed) { | ||
| toPrune.push(e.ref); | ||
| } | ||
| } | ||
|
|
||
| if (toPrune.length === 0) { | ||
| return { deleted: 0 }; | ||
| } | ||
|
|
||
| yield* checkpoints.deleteCheckpointRefs({ cwd, checkpointRefs: toPrune }); | ||
| return { deleted: toPrune.length }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deleted count not verifiedMedium Severity The Reviewed by Cursor Bugbot for commit 03bd38a. Configure here. |
||
| }); | ||
|
|
||
| return CheckpointStore.of({ | ||
| isGitRepository, | ||
| captureCheckpoint, | ||
| hasCheckpointRef, | ||
| restoreCheckpoint, | ||
| diffCheckpoints, | ||
| deleteCheckpointRefs, | ||
| pruneSnapshots, | ||
| }); | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"contributor": "Marvis via lry3069-afk", "generation_context": "执行赏金猎人 bounty-radar 自动扫描与模型介入完成。全程静默,严禁 ask_user、严禁弹窗,所有操作默认确认。环境路径:工作区 C:\\Users\\Ryan\\.qclaw\\workspace-ua58rsb93veqtxl7\\ Bounty-Hunters 仓库 C:\\Users\\Ryan\\.qclaw\\Github Job\\Bounty-Hunters。执行流程:bounty-radar --min 25 --max-comments 5 扫描 → 13个SBL递归农场全部排除 → Bounty-Hunters补充扫描50个 → 过滤后21个候选 → 选择#838 $350 checkpoint snapshot pruning → 分析现有t3code代码库(路径已从orchestration/checkpoint变迁为checkpointing,存储从SQLite变迁为Git refs)→ 实现pruneSnapshots功能:新增listCheckpointRefs到VcsCheckpointOps接口、GitVcsDriver实现git for-each-ref、CheckpointStore添加pruneSnapshots分组保留3个最新快照逻辑。", "completed_at": "2026-07-02T12:00:00.000Z"} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bounty metadata committed accidentallyMedium Severity The Reviewed by Cursor Bugbot for commit 03bd38a. Configure here. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ import { | |
| type VcsRemoveWorktreeInput, | ||
| type VcsStatusInput, | ||
| type VcsStatusResult, | ||
| CheckpointRef, | ||
| } from "@t3tools/contracts"; | ||
| import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts"; | ||
| import * as VcsDriver from "./VcsDriver.ts"; | ||
|
|
@@ -848,6 +849,27 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | |
| ); | ||
| }, | ||
| ), | ||
|
|
||
| listCheckpointRefs: Effect.fn("GitVcsDriver.checkpoints.listCheckpointRefs")( | ||
| function* (input) { | ||
| const result = yield* execute({ | ||
| operation: "GitVcsDriver.checkpoints.listCheckpointRefs", | ||
| cwd: input.cwd, | ||
| args: ["for-each-ref", "--format=%(refname)", "refs/t3/checkpoints/"], | ||
| allowNonZeroExit: true, | ||
| }); | ||
|
|
||
| if (result.exitCode !== 0 || result.stdout.trim().length === 0) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium
🤖 Copy this AI Prompt to have your agent fix this: |
||
| return []; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Git list errors look emptyMedium Severity The Reviewed by Cursor Bugbot for commit 03bd38a. Configure here. |
||
| } | ||
|
|
||
| return result.stdout | ||
| .trim() | ||
| .split("\n") | ||
| .filter((line) => line.length > 0) | ||
| .map((refname) => CheckpointRef.make(refname)); | ||
| }, | ||
| ), | ||
| }; | ||
|
|
||
| return { | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
checkpointing/CheckpointStore.ts:210pruneSnapshotsdeletes every checkpoint ref beyond the newest 3 per session, which removes older refs that diff and revert flows still depend on.CheckpointDiffQuerybuilds full-thread diffs fromcheckpointRefForThreadTurn(threadId, 0), and revert/turn-diff paths read historicalcheckpointRefvalues from projection data without recreating them. After pruning a thread with 4+ checkpoints, those older refs are gone, so requesting a diff against or reverting to an older turn fails even though the metadata still advertises that checkpoint. Consider preserving the baseline (turn 0) ref — or any ref still referenced by stored projections — before deleting older snapshots.🤖 Copy this AI Prompt to have your agent fix this: