Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1dddeba
feat(checkpoints): per-write checkpoints, task-start baseline, and pe…
easonliang28 Aug 27, 2026
b35b231
fix(fws): export the webview initializer and test the pre-hydration c…
easonliang28 Aug 28, 2026
4277a90
chore(ci): empty commit — re-trigger CI and the CodeRabbit current-he…
easonliang28 Aug 30, 2026
86038f2
fix(checkpoints): skip task-start baseline on history resume
easonliang28 Oct 12, 2026
a5e8ff2
fix(checkpoints): skip baseline on aborted task; share default constant
easonliang28 Oct 12, 2026
10a7e36
fix(checkpoints): recheck cancellation after the baseline state lookup
easonliang28 Oct 12, 2026
c37d0cb
fix(checkpoints): await write-path checkpoint saves before state handoff
easonliang28 Oct 12, 2026
b56d978
test(checkpoints): pin one-checkpoint-per-patch for multi-file patches
easonliang28 Oct 12, 2026
f3d58d3
fix(tools): defer the queued-message handoff until the patch checkpoi…
easonliang28 Oct 13, 2026
d52a333
fix(task): never dequeue queued messages for an aborted task
easonliang28 Oct 13, 2026
ee21d69
test(tools): pin the partial-failure handoff semantics of the patch c…
easonliang28 Oct 13, 2026
78c32c1
chore: remove carried-over Stryker suppression directives from touche…
easonliang28 Oct 14, 2026
3a6a0b4
test: strengthen the early-return handoff assertions
easonliang28 Oct 15, 2026
42e34d8
test: assert the in-place update checkpoint contract
easonliang28 Oct 15, 2026
18b7c63
fix(apply-patch): fail moves when source deletion fails; checkpoint p…
easonliang28 Oct 16, 2026
2e3a042
fix(tools): checkpoint retained writes before handoff after a handler…
easonliang28 Oct 17, 2026
530bc40
fix(task): task-start checkpoint baseline errors no longer abort task…
easonliang28 Oct 17, 2026
f66debc
fix(checkpoints): restore a clobbered move destination when source de…
easonliang28 Oct 18, 2026
3eda2b3
fix(checkpoints): harden task-start baseline and service init per review
easonliang28 Oct 18, 2026
9bf5854
fix(task): bound the task-start baseline save; report a failed move r…
Oct 21, 2026
bbf67b4
fix(tools): make the apply_patch move transactional with atomic stagi…
easonLiangWorldedtech Oct 22, 2026
f452069
fix(tools): preserve destination identity and permissions in the atom…
easonLiangWorldedtech Oct 22, 2026
4ec44f6
fix(checkpoints): serialize per-task checkpoint saves
easonLiangWorldedtech Oct 22, 2026
a033ac1
fix(tools): keep staging owner-only and make partial patch mutations …
easonLiangWorldedtech Oct 22, 2026
fa00851
style: apply prettier formatting to the staging and mutation-tracking…
easonLiangWorldedtech Oct 22, 2026
c50616f
fix(tools): track committed mutations outside the handler-return boun…
easonLiangWorldedtech Oct 22, 2026
2a04a03
chore: re-trigger CI after the e2e restart-phase timeout
easonLiangWorldedtech Oct 22, 2026
f1df46e
test+fix(checkpoints): correct the delete-ordering mock, kill the han…
easonLiangWorldedtech Nov 8, 2026
f994397
fix(per-write-checkpoints): lock restores, track retained writes, nam…
easonLiangWorldedtech Nov 9, 2026
8dab18c
test(task): raise the abort latch through requestAbort in the three c…
easonLiangWorldedtech Nov 9, 2026
0731763
Fix the two checkpoint-queue findings the review raised, and stop a s…
easonLiangWorldedtech Nov 9, 2026
84c9973
test(webview): carry perWriteCheckpoints in the postMessageToWebview …
Nov 10, 2026
b659c1e
test(vscode-e2e): regenerate the chat-dark visual baseline for the ac…
easonLiangWorldedtech Nov 11, 2026
8ce6f90
test(checkpoints): assert shadow git contents for checkpointSave
easonLiangWorldedtech Nov 11, 2026
8a779ea
fix(checkpoints): hand an expired caller back to the initialization s…
easonLiangWorldedtech Nov 11, 2026
872f563
fix(checkpoints): decline to publish a baseline a write overtook rath…
easonLiangWorldedtech Nov 11, 2026
0c72481
style(checkpoints): format the premise gate and its test
easonLiangWorldedtech Nov 11, 2026
e1f7c97
test(checkpoints): cover the dropped save and the retry behind it in …
easonLiangWorldedtech Nov 11, 2026
5523228
fix(checkpoints): let the premise setup infer its return type rather …
easonLiangWorldedtech Nov 11, 2026
073bb80
fix(apply-patch): write a move through a destination symlink
easonLiangWorldedtech Nov 11, 2026
d25a24b
test(apply-patch): carry toolCallId in the callback fixtures at the r…
easonLiangWorldedtech Nov 11, 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@
*/
export const DEFAULT_CHECKPOINT_TIMEOUT_SECONDS = 15

/**
* Whether per-write checkpoints and task-start baseline are enabled by default.
* Master switch for the B cluster of checkpoint features.
* @default true
*/
export const DEFAULT_PER_WRITE_CHECKPOINTS = true

Check warning on line 122 in packages/types/src/global-settings.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/global-settings.ts:122: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/**
* GlobalSettings
*/
Expand Down Expand Up @@ -223,6 +230,12 @@
.min(MIN_CHECKPOINT_TIMEOUT_SECONDS)
.max(MAX_CHECKPOINT_TIMEOUT_SECONDS)
.optional(),
/**
* Whether to record a shadow-git checkpoint after every successful write_to_file,
* edit_file, and apply_patch (per-write checkpoints), plus a task-start baseline.
* @default true
*/
perWriteCheckpoints: z.boolean().optional(),

ttsEnabled: z.boolean().optional(),
ttsSpeed: z.number().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export type ExtensionState = Pick<

enableCheckpoints: boolean
checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15)
perWriteCheckpoints: boolean
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
Expand Down
208 changes: 207 additions & 1 deletion src/core/checkpoints/__tests__/checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,179 @@ describe("Checkpoint functionality", () => {
})

describe("checkpointSave", () => {
it("serializes concurrent saves for the same task so stageAll + commit never overlap", async () => {
const order: string[] = []
let resolveFirst: (value: { commit: string }) => void = () => {}
mockCheckpointService.saveCheckpoint
.mockImplementationOnce(() => {
order.push("first:start")
return new Promise((resolve) => {
resolveFirst = resolve
})
})
.mockImplementationOnce(async () => {
order.push("second:start")
return { commit: "second-commit" }
})

const first = checkpointSave(mockTask, false, true)
const second = checkpointSave(mockTask, false, true)

// The second save must not reach the service while the first is still running:
// a queued save owns the stageAll + commit pair until it settles.
await new Promise((resolve) => setTimeout(resolve, 10))
expect(order).toEqual(["first:start"])

resolveFirst({ commit: "first-commit" })
await Promise.all([first, second])
expect(order).toEqual(["first:start", "second:start"])
})

it("drops a queued save whose checkpoint window expired behind a stalled predecessor", async () => {
// A stalled baseline releases the task after checkpointTimeout, but the queue used to
// start it anyway, however late, committing whatever the workspace had become by then.
// It must not: the save is dropped and the caller resolves with no commit.
mockTask.checkpointTimeout = 0.01
const order: string[] = []
let resolveFirst: (value: { commit: string }) => void = () => {}
mockCheckpointService.saveCheckpoint
.mockImplementationOnce(() => {
order.push("stalled:start")
return new Promise((resolve) => {
resolveFirst = resolve
})
})
.mockImplementation(async () => {
order.push("late:start")
return { commit: "late-commit" }
})
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})

const stalled = checkpointSave(mockTask, true, true)
const queued = await (async () => {
await new Promise((resolve) => setTimeout(resolve, 5))
const second = checkpointSave(mockTask, false, true)
// Give the expired request its turn behind the still-running baseline, then release
// the baseline so the queue drains.
await new Promise((resolve) => setTimeout(resolve, 30))
resolveFirst({ commit: "stalled-commit" })
return second
})()

const second = await queued
await stalled
// Only the stalled baseline ever reached the shadow git; the expired save never started,
// so no commit describes a workspace that has since moved on.
expect(order).toEqual(["stalled:start"])
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledTimes(1)
expect(second).toBeUndefined()
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("skipping queued per-write checkpoint save"))
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("expired behind a slower checkpoint"))
warnSpy.mockRestore()
})

it("drops a queued save for a task that was canceled while it waited", async () => {
mockTask.checkpointTimeout = 15
const order: string[] = []
let resolveFirst: (value: { commit: string }) => void = () => {}
mockCheckpointService.saveCheckpoint
.mockImplementationOnce(() => {
order.push("stalled:start")
return new Promise((resolve) => {
resolveFirst = resolve
})
})
.mockImplementation(async () => {
order.push("canceled:start")
return { commit: "canceled-commit" }
})
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
mockTask.abort = false

const stalled = checkpointSave(mockTask, true, true)
const queued = checkpointSave(mockTask, false, true)
await new Promise((resolve) => setTimeout(resolve, 5))
// Cancellation arrives while the save is queued, before its turn.
mockTask.abort = true
resolveFirst({ commit: "stalled-commit" })

const second = await queued
await stalled

expect(order).toEqual(["stalled:start"])
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledTimes(1)
expect(second).toBeUndefined()
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("task canceled"))
warnSpy.mockRestore()
mockTask.abort = false
})

it("runs a save requested after a stalled predecessor settles, exactly once each", async () => {
// The gate must not poison the queue: the dropped request stays dropped, while a fresh
// request after the stall settles gets its normal turn.
mockTask.checkpointTimeout = 0.01
const order: string[] = []
let resolveFirst: (value: { commit: string }) => void = () => {}
mockCheckpointService.saveCheckpoint
.mockImplementationOnce(() => {
order.push("stalled:start")
return new Promise((resolve) => {
resolveFirst = resolve
})
})
.mockImplementation(async () => {
order.push("fresh:start")
return { commit: "fresh-commit" }
})
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})

const stalled = checkpointSave(mockTask, true, true)
// Requested while its window is still open, so the drop below is about the wait behind
// the stalled baseline rather than about an already-expired request.
const expired = checkpointSave(mockTask, false, true)
await new Promise((resolve) => setTimeout(resolve, 30))
resolveFirst({ commit: "stalled-commit" })
const expiredResult = await expired
const settled = await stalled
const fresh = await checkpointSave(mockTask, false, true)

expect(order).toEqual(["stalled:start", "fresh:start"])
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledTimes(2)
// The dropped request resolves exactly the way a missing service does: no commit value,
// while the predecessor that held the queue keeps its own result.
expect(expiredResult).toBeUndefined()
expect(settled).toEqual({ commit: "stalled-commit" })
expect(fresh).toEqual({ commit: "fresh-commit" })
warnSpy.mockRestore()
})

it("never drops a user-initiated restore queued behind a stalled save", async () => {
// Restores carry no expiry: dropping one would silently discard an explicit user action.
mockTask.checkpointTimeout = 0.01
mockTask.clineMessages = [{ ts: 2, type: "say", say: "checkpoint_saved", text: RESTORE_HASH }]
const order: string[] = []
let resolveSave: (value: { commit: string }) => void = () => {}
mockCheckpointService.saveCheckpoint.mockImplementationOnce(() => {
order.push("save:start")
return new Promise((resolve) => {
resolveSave = resolve
})
})
mockCheckpointService.restoreCheckpoint.mockImplementationOnce(() => {
order.push("restore:start")
return Promise.resolve(undefined)
})

const stalled = checkpointSave(mockTask, true, true)
const restore = checkpointRestore(mockTask, { ts: 2, commitHash: RESTORE_HASH, mode: "preview" })
await new Promise((resolve) => setTimeout(resolve, 30))
resolveSave({ commit: "stalled-commit" })
await Promise.all([stalled, restore])

expect(order).toEqual(["save:start", "restore:start"])
expect(mockCheckpointService.restoreCheckpoint).toHaveBeenCalledWith(RESTORE_HASH)
})

it("should wait for checkpoint service initialization before saving", async () => {
// Set up task with uninitialized service
mockCheckpointService.isInitialized = false
Expand All @@ -129,9 +302,11 @@ describe("Checkpoint functionality", () => {
const result = await savePromise

// saveCheckpoint should have been called
// The baseline carries a content premise now: it describes the workspace before any agent
// write, and saveCheckpoint declines to publish if a write landed before it staged.
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
expect.stringContaining("Task: test-task-id"),
{ allowEmpty: true, suppressMessage: false },
{ allowEmpty: true, suppressMessage: false, contentPremise: expect.any(Function) },
)

// Result should contain the commit hash
Expand Down Expand Up @@ -280,6 +455,37 @@ describe("Checkpoint functionality", () => {
expect(mockCheckpointService.restoreCheckpoint).not.toHaveBeenCalled()
})

it("serializes a restore against a queued save so no save stages over a restore", async () => {
const order: string[] = []
let resolveRestore: () => void = () => {}
mockCheckpointService.restoreCheckpoint.mockImplementationOnce(
() =>
new Promise((resolve) => {
order.push("restore:start")
resolveRestore = resolve as () => void
}),
)
mockCheckpointService.saveCheckpoint.mockImplementationOnce(async () => {
order.push("save:start")
return { commit: "queued-commit" }
})

const restore = checkpointRestore(mockTask, { ts: 2, commitHash: RESTORE_HASH, mode: "preview" })
// Let the restore reach the shadow-git reset.
await new Promise((resolve) => setTimeout(resolve, 10))

const save = checkpointSave(mockTask, false, true)
// The save must not reach stageAll + commit while the restore is resetting the
// workspace: that commit would record the restored bytes and drop the write's own
// checkpoint.
await new Promise((resolve) => setTimeout(resolve, 10))
expect(order).toEqual(["restore:start"])

resolveRestore()
await Promise.all([restore, save])
expect(order).toEqual(["restore:start", "save:start"])
})

it.each(["delete", "edit"] as const)(
"propagates restore failures for %s without rewinding",
async (operation) => {
Expand Down
Loading
Loading