feat(boulder-state): add session_origins tracking for direct vs appended sessions
- Add session_origins field to BoulderState type
- Track direct vs appended session origins in storage layer
- Add migration logic for existing state files
- Add comprehensive tests for session origin tracking
🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
@@ -118,6 +118,40 @@ describe("boulder-state", () => {
|
|||||||
expect(result!.session_ids).toEqual([])
|
expect(result!.session_ids).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should backfill missing origin as direct only for a single tracked session", () => {
|
||||||
|
// given
|
||||||
|
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
|
||||||
|
writeFileSync(boulderFile, JSON.stringify({
|
||||||
|
active_plan: "/path/to/plan.md",
|
||||||
|
started_at: "2026-01-01T00:00:00Z",
|
||||||
|
session_ids: ["session-1"],
|
||||||
|
plan_name: "plan",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result?.session_origins).toEqual({ "session-1": "direct" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should keep missing origins empty when multiple sessions are tracked", () => {
|
||||||
|
// given
|
||||||
|
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
|
||||||
|
writeFileSync(boulderFile, JSON.stringify({
|
||||||
|
active_plan: "/path/to/plan.md",
|
||||||
|
started_at: "2026-01-01T00:00:00Z",
|
||||||
|
session_ids: ["session-1", "session-2"],
|
||||||
|
plan_name: "plan",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = readBoulderState(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result?.session_origins).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
test("should read valid boulder state", () => {
|
test("should read valid boulder state", () => {
|
||||||
// given - valid boulder.json
|
// given - valid boulder.json
|
||||||
const state: BoulderState = {
|
const state: BoulderState = {
|
||||||
@@ -239,6 +273,26 @@ describe("boulder-state", () => {
|
|||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(result!.session_ids).toContain("ses-new")
|
expect(result!.session_ids).toContain("ses-new")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should persist appended session origin when provided", () => {
|
||||||
|
// given
|
||||||
|
writeBoulderState(TEST_DIR, {
|
||||||
|
active_plan: "/path/to/plan.md",
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: ["session-1"],
|
||||||
|
session_origins: { "session-1": "direct" },
|
||||||
|
plan_name: "plan",
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = appendSessionId(TEST_DIR, "session-2", "appended")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result?.session_origins).toEqual({
|
||||||
|
"session-1": "direct",
|
||||||
|
"session-2": "appended",
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("clearBoulderState", () => {
|
describe("clearBoulderState", () => {
|
||||||
@@ -547,6 +601,18 @@ describe("boulder-state", () => {
|
|||||||
expect(state.plan_name).toBe("feature")
|
expect(state.plan_name).toBe("feature")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should mark the initial session origin as direct", () => {
|
||||||
|
// given
|
||||||
|
const planPath = "/path/to/feature.md"
|
||||||
|
const sessionId = "ses-origin"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const state = createBoulderState(planPath, sessionId)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(state.session_origins).toEqual({ [sessionId]: "direct" })
|
||||||
|
})
|
||||||
|
|
||||||
test("should allow agent to be undefined", () => {
|
test("should allow agent to be undefined", () => {
|
||||||
//#given - plan path and session id without agent
|
//#given - plan path and session id without agent
|
||||||
const planPath = "/path/to/legacy.md"
|
const planPath = "/path/to/legacy.md"
|
||||||
|
|||||||
@@ -31,6 +31,19 @@ export function readBoulderState(directory: string): BoulderState | null {
|
|||||||
if (!Array.isArray(parsed.session_ids)) {
|
if (!Array.isArray(parsed.session_ids)) {
|
||||||
parsed.session_ids = []
|
parsed.session_ids = []
|
||||||
}
|
}
|
||||||
|
if (!parsed.session_origins || typeof parsed.session_origins !== "object" || Array.isArray(parsed.session_origins)) {
|
||||||
|
parsed.session_origins = {}
|
||||||
|
}
|
||||||
|
if (parsed.session_ids.length === 1) {
|
||||||
|
const soleSessionId = parsed.session_ids[0]
|
||||||
|
if (
|
||||||
|
typeof soleSessionId === "string"
|
||||||
|
&& parsed.session_origins[soleSessionId] !== "appended"
|
||||||
|
&& parsed.session_origins[soleSessionId] !== "direct"
|
||||||
|
) {
|
||||||
|
parsed.session_origins[soleSessionId] = "direct"
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!parsed.task_sessions || typeof parsed.task_sessions !== "object" || Array.isArray(parsed.task_sessions)) {
|
if (!parsed.task_sessions || typeof parsed.task_sessions !== "object" || Array.isArray(parsed.task_sessions)) {
|
||||||
parsed.task_sessions = {}
|
parsed.task_sessions = {}
|
||||||
}
|
}
|
||||||
@@ -56,23 +69,41 @@ export function writeBoulderState(directory: string, state: BoulderState): boole
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendSessionId(directory: string, sessionId: string): BoulderState | null {
|
export function appendSessionId(
|
||||||
|
directory: string,
|
||||||
|
sessionId: string,
|
||||||
|
origin: "direct" | "appended" = "direct",
|
||||||
|
): BoulderState | null {
|
||||||
const state = readBoulderState(directory)
|
const state = readBoulderState(directory)
|
||||||
if (!state) return null
|
if (!state) return null
|
||||||
|
|
||||||
|
if (!state.session_origins || typeof state.session_origins !== "object" || Array.isArray(state.session_origins)) {
|
||||||
|
state.session_origins = {}
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.session_ids?.includes(sessionId)) {
|
if (!state.session_ids?.includes(sessionId)) {
|
||||||
if (!Array.isArray(state.session_ids)) {
|
if (!Array.isArray(state.session_ids)) {
|
||||||
state.session_ids = []
|
state.session_ids = []
|
||||||
}
|
}
|
||||||
const originalSessionIds = [...state.session_ids]
|
const originalSessionIds = [...state.session_ids]
|
||||||
|
const originalSessionOrigins = { ...state.session_origins }
|
||||||
state.session_ids.push(sessionId)
|
state.session_ids.push(sessionId)
|
||||||
|
state.session_origins[sessionId] = origin
|
||||||
if (writeBoulderState(directory, state)) {
|
if (writeBoulderState(directory, state)) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
state.session_ids = originalSessionIds
|
state.session_ids = originalSessionIds
|
||||||
|
state.session_origins = originalSessionOrigins
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.session_origins[sessionId]) {
|
||||||
|
state.session_origins[sessionId] = origin
|
||||||
|
if (!writeBoulderState(directory, state)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +244,9 @@ export function createBoulderState(
|
|||||||
active_plan: planPath,
|
active_plan: planPath,
|
||||||
started_at: new Date().toISOString(),
|
started_at: new Date().toISOString(),
|
||||||
session_ids: [sessionId],
|
session_ids: [sessionId],
|
||||||
|
session_origins: {
|
||||||
|
[sessionId]: "direct",
|
||||||
|
},
|
||||||
plan_name: getPlanName(planPath),
|
plan_name: getPlanName(planPath),
|
||||||
...(agent !== undefined ? { agent } : {}),
|
...(agent !== undefined ? { agent } : {}),
|
||||||
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
|
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface BoulderState {
|
|||||||
started_at: string
|
started_at: string
|
||||||
/** Session IDs that have worked on this plan */
|
/** Session IDs that have worked on this plan */
|
||||||
session_ids: string[]
|
session_ids: string[]
|
||||||
|
session_origins?: Record<string, "direct" | "appended">
|
||||||
/** Plan name derived from filename */
|
/** Plan name derived from filename */
|
||||||
plan_name: string
|
plan_name: string
|
||||||
/** Agent type to use when resuming (e.g., 'atlas') */
|
/** Agent type to use when resuming (e.g., 'atlas') */
|
||||||
|
|||||||
Reference in New Issue
Block a user