Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
1a362e5
fix: prevent stale cross-window subtask completion
roomote Aug 31, 2026
cd1ab1b
test(task): cover cross-window handoff failures
roomote Sep 3, 2026
39273bf
test(task): cover remaining handoff guards
roomote Sep 3, 2026
397ee60
refactor(task): keep mutation scope focused
roomote Sep 3, 2026
36d3c1b
refactor(task): fit changed-code mutation cap
roomote Sep 3, 2026
494510e
test(task): align real lock concurrency coverage
roomote Sep 3, 2026
5ed077c
refactor(task): compose locked delegation transition
roomote Sep 3, 2026
8e8ae53
test(task): expose delegation suites to mutation gate
roomote Sep 3, 2026
cd94b7b
fix(task): compensate failed delegated handoffs
roomote Sep 3, 2026
adcc833
refactor(task): keep compensation mutation-focused
roomote Sep 3, 2026
70eb6d5
refactor(task): fit compensated mutation scope
roomote Sep 3, 2026
43718c5
test(task): close changed-code mutation gaps
roomote Sep 3, 2026
c2e2bc9
refactor(task): make disk guards mutation-visible
roomote Sep 3, 2026
376fa4d
test(task): verify cross-host handoff protocol
roomote Sep 4, 2026
07050d9
fix(task): address latest concurrency review
roomote Sep 7, 2026
16f5aa0
refactor(task): keep reviewed mutation scope bounded
roomote Sep 7, 2026
f6a21f0
test(task): cover caller-held lock rollback
roomote Sep 7, 2026
87831d3
fix(task): integrate latest lifecycle persistence
roomote Sep 7, 2026
e006bc9
refactor(task): compose latest locked handoff
roomote Sep 7, 2026
ad9df9a
test(task): cover latest locked handoff branches
roomote Sep 7, 2026
f1a33c4
test(task): cover lock failure without recovery hook
roomote Sep 7, 2026
6955a3b
fix(task): retain backup after lock compromise
roomote Sep 10, 2026
81011f8
refactor(task): keep compromised backup guard narrow
roomote Sep 10, 2026
1040478
refactor(task): minimize retained backup path
roomote Sep 10, 2026
4e3b29a
refactor(task): log retained backup compactly
roomote Sep 10, 2026
c4d5f43
fix(task): retain backup after rollback failure
roomote Sep 10, 2026
5fa2a7b
test(task): verify delegated child startup
roomote Sep 10, 2026
f3c9cd5
fix(task): preserve compromised lock recovery
roomote Sep 10, 2026
f7f8e50
refactor(task): narrow recovery cleanup flow
roomote Sep 10, 2026
3cd12ea
test(task): cover recovery retention boundaries
roomote Sep 10, 2026
353cdd8
test(task): close recovery mutation gaps
roomote Sep 10, 2026
c7a8e43
test(task): cover lock outcome arbitration
roomote Sep 10, 2026
29c904b
refactor(task): consolidate handoff recovery
roomote Sep 11, 2026
bddab5e
refactor(task): keep handoff diff localized
roomote Sep 11, 2026
9a27ef9
refactor(task): reuse guarded preimage restore
roomote Sep 11, 2026
e4edd77
refactor(task): collapse duplicate transition branches
roomote Sep 11, 2026
bfaa8db
refactor(task): preserve narrow mutation ranges
roomote Sep 11, 2026
f218320
refactor(task): flatten persistence branches
roomote Sep 11, 2026
c325f9b
refactor(task): remove incidental merge mode
roomote Sep 11, 2026
8bc72e0
refactor(task): specialize locked handoff runner
roomote Sep 11, 2026
6dcc588
refactor(task): inline atomic pair temporaries
roomote Sep 11, 2026
564de51
refactor(task): use atomic delegation lock directly
roomote Sep 11, 2026
af17eb2
test(task): cover completed cancellation cleanup
roomote Sep 11, 2026
09cc215
refactor(task): finalize atomic rollback contract
roomote Sep 11, 2026
12ffff7
test(task): model callback failure prefixes
roomote Sep 11, 2026
81d986c
fix(task): close pre-merge recovery gaps
roomote Sep 11, 2026
994439c
test(task): close review concurrency gaps
roomote Sep 11, 2026
7dccc28
refactor(task): compact ambiguous compensation guards
roomote Sep 11, 2026
6f1ebf4
refactor(task): restore compact compensation selection
roomote Sep 11, 2026
a35c13a
refactor(task): keep mutation scope bounded
roomote Sep 11, 2026
327d1ac
refactor(task): reuse direct lifecycle paths
roomote Sep 11, 2026
5aad420
fix(task): preserve explicit removal and compromise diagnostics
roomote Sep 11, 2026
f603e1d
refactor(task): simplify guarded failure handling
roomote Sep 11, 2026
7045443
refactor(task): share delegation guards
roomote Sep 11, 2026
82b967e
refactor(task): preserve compensation arrays
roomote Sep 11, 2026
088ec76
refactor(task): compact compensation tuples
roomote Sep 11, 2026
a4e4d82
test(task): kill compensation and handoff mutants
roomote Sep 11, 2026
54e7f28
fix(task): restore absent pair preimages safely
roomote Sep 12, 2026
fe35e94
refactor(task): compact preimage states
roomote Sep 12, 2026
ae413ea
refactor(task): consolidate locked persistence paths
roomote Sep 12, 2026
d163fdf
refactor(task): keep persistence mutation scope bounded
roomote Sep 12, 2026
7405b1c
refactor(task): compact pair invariant errors
roomote Sep 12, 2026
b3e447c
refactor(task): compact compensation reporting
roomote Sep 12, 2026
ec870b9
refactor(core): centralize task preimage validation
roomote Sep 12, 2026
9876418
test(task): cover preimage restoration branches
roomote Sep 12, 2026
5274440
test(task): harden compensation mutation coverage
roomote Sep 12, 2026
97144b4
test(task): cover final compensation mutants
roomote Sep 12, 2026
c137e30
refactor(core): build expected preimage states
roomote Sep 12, 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
53 changes: 31 additions & 22 deletions docs/architecture/task-lifecycle-model.md

Large diffs are not rendered by default.

46 changes: 45 additions & 1 deletion packages/core/src/task-history/__tests__/task-history.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ import * as fs from "fs/promises"
import * as os from "os"
import * as path from "path"

import { readTaskSessionsFromStoragePath } from "../index.js"
import type { HistoryItem } from "@roo-code/types"

import {
ABSENT_TASK_FILE_PREIMAGE,
INVALID_TASK_FILE_PREIMAGE,
expectedTaskFileStates,
isValidTaskFilePreImage,
matchesExpectedHistoryItem,
readTaskSessionsFromStoragePath,
taskFilePreImage,
} from "../index.js"

describe("readTaskSessionsFromStoragePath", () => {
let tempDir: string
Expand Down Expand Up @@ -112,3 +122,37 @@ describe("readTaskSessionsFromStoragePath", () => {
await expect(readTaskSessionsFromStoragePath(tempDir)).resolves.toEqual([])
})
})

describe("task file pre-images", () => {
const item: HistoryItem = {
id: "task-1",
number: 1,
ts: 1,
task: "Task",
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
}

it("distinguishes validated, absent, and invalid pre-images", () => {
const valid = taskFilePreImage(item, item.id, true)
expect(valid).toEqual(item)
expect(valid).not.toBe(item)
expect(taskFilePreImage(null, item.id, false)).toBe(ABSENT_TASK_FILE_PREIMAGE)
expect(taskFilePreImage(null, item.id, true)).toBe(INVALID_TASK_FILE_PREIMAGE)
expect(taskFilePreImage({ ...item, id: "other" }, item.id, true)).toBe(INVALID_TASK_FILE_PREIMAGE)
expect(taskFilePreImage({ ...item, id: "other" }, item.id, false)).toBe(INVALID_TASK_FILE_PREIMAGE)
expect(taskFilePreImage({ id: item.id }, item.id, true)).toBe(INVALID_TASK_FILE_PREIMAGE)
})

it("recognizes valid snapshots and expected records", () => {
expect(isValidTaskFilePreImage(item)).toBe(true)
expect(isValidTaskFilePreImage(ABSENT_TASK_FILE_PREIMAGE)).toBe(false)
expect(isValidTaskFilePreImage(INVALID_TASK_FILE_PREIMAGE)).toBe(false)
expect(expectedTaskFileStates(item, { ...item })).toEqual([item, item])
expect(expectedTaskFileStates(item, ABSENT_TASK_FILE_PREIMAGE)).toEqual([item])
expect(expectedTaskFileStates(item, INVALID_TASK_FILE_PREIMAGE)).toEqual([item])
expect(matchesExpectedHistoryItem(item, [{ ...item }], (left, right) => left.id === right.id)).toBe(true)
expect(matchesExpectedHistoryItem(item, [], (left, right) => left.id === right.id)).toBe(false)
})
})
30 changes: 29 additions & 1 deletion packages/core/src/task-history/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from "fs/promises"
import * as path from "path"

import type { HistoryItem } from "@roo-code/types"
import { historyItemSchema, type HistoryItem } from "@roo-code/types"

const HISTORY_ITEM_FILENAME = "history_item.json"
const HISTORY_INDEX_FILENAME = "_index.json"
Expand All @@ -15,6 +15,34 @@ export interface TaskSessionEntry {
status?: HistoryItem["status"]
}

export const ABSENT_TASK_FILE_PREIMAGE = "absent" as const
export const INVALID_TASK_FILE_PREIMAGE = "invalid" as const
export type TaskFilePreImage = HistoryItem | typeof ABSENT_TASK_FILE_PREIMAGE | typeof INVALID_TASK_FILE_PREIMAGE

export function taskFilePreImage(existing: unknown, taskId: string, fileExists: boolean): TaskFilePreImage {
const parsed = historyItemSchema.safeParse(existing)
if (parsed.success && parsed.data.id === taskId) return structuredClone(existing as HistoryItem)
if (existing !== null) return INVALID_TASK_FILE_PREIMAGE
if (fileExists) return INVALID_TASK_FILE_PREIMAGE
return ABSENT_TASK_FILE_PREIMAGE
}

export function isValidTaskFilePreImage(preImage: TaskFilePreImage): preImage is HistoryItem {
return preImage !== ABSENT_TASK_FILE_PREIMAGE && preImage !== INVALID_TASK_FILE_PREIMAGE
}

export function expectedTaskFileStates(written: HistoryItem, preImage: TaskFilePreImage): HistoryItem[] {
return isValidTaskFilePreImage(preImage) ? [written, preImage] : [written]
}

export function matchesExpectedHistoryItem(
item: HistoryItem,
expected: readonly HistoryItem[],
equals: (left: HistoryItem, right: HistoryItem) => boolean,
): boolean {
return expected.some((candidate) => equals(item, candidate))
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
Expand Down
Loading
Loading