feat(boulder-state): add session-aware multi-work storage helpers

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-11 13:28:58 +09:00
parent 246e0dca80
commit 9f500743d1
2 changed files with 651 additions and 5 deletions
+157
View File
@@ -3,17 +3,28 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { dirname, join } from "node:path"
import { tmpdir } from "node:os"
import {
addBoulderWork,
appendSessionIdForWork,
getActiveWorks,
getBoulderWorks,
readBoulderState,
writeBoulderState,
appendSessionId,
clearBoulderState,
getWorkById,
getWorkByPlanName,
getWorkForSession,
getWorkResumeOptions,
getPlanProgress,
getPlanName,
createBoulderState,
findPrometheusPlans,
getTaskSessionState,
resolveBoulderPlanPath,
resolveBoulderPlanPathForWork,
selectActiveWork,
upsertTaskSessionState,
upsertTaskSessionStateForWork,
} from "./storage"
import type { BoulderState } from "./types"
import { readCurrentTopLevelTask } from "./top-level-task"
@@ -39,6 +50,31 @@ describe("boulder-state", () => {
})
describe("readBoulderState", () => {
test("should preserve legacy boulder.json fields during round-trip", () => {
// given
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const legacyRawState = {
active_plan: "/path/to/legacy-plan.md",
started_at: "2026-01-01T00:00:00.000Z",
session_ids: ["legacy-session"],
plan_name: "legacy-plan",
}
writeFileSync(boulderFile, JSON.stringify(legacyRawState, null, 2), "utf-8")
// when
const state = readBoulderState(TEST_DIR)
expect(state).not.toBeNull()
const writeSucceeded = writeBoulderState(TEST_DIR, state!)
const roundTripState = readBoulderState(TEST_DIR)
// then
expect(writeSucceeded).toBe(true)
expect(roundTripState?.active_plan).toBe(legacyRawState.active_plan)
expect(roundTripState?.started_at).toBe(legacyRawState.started_at)
expect(roundTripState?.session_ids).toEqual(legacyRawState.session_ids)
expect(roundTripState?.plan_name).toBe(legacyRawState.plan_name)
})
test("should return null when no boulder.json exists", () => {
// given - no boulder.json file
// when
@@ -387,6 +423,127 @@ describe("boulder-state", () => {
})
})
describe("multi-work helpers", () => {
test("should add second work and keep both active works", () => {
// given
const firstState = createBoulderState(
join(TEST_DIR, ".sisyphus/plans/plan-a.md"),
"session-a",
"atlas",
"/worktree-a",
)
writeBoulderState(TEST_DIR, firstState)
const firstWorkId = firstState.active_work_id
// when
const updatedState = addBoulderWork(TEST_DIR, {
planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"),
sessionId: "session-b",
agent: "atlas",
worktreePath: "/worktree-b",
})
// then
expect(updatedState).not.toBeNull()
const works = updatedState?.works ?? {}
expect(Object.keys(works).length).toBe(2)
expect(firstWorkId).toBeDefined()
expect(works[firstWorkId!]).toBeDefined()
expect(updatedState?.active_plan).toContain("plan-b.md")
expect(getActiveWorks(TEST_DIR).length).toBe(2)
})
test("should resolve work for session using updated_at tie-break", () => {
// given
const baseState = createBoulderState(
join(TEST_DIR, ".sisyphus/plans/plan-a.md"),
"session-a",
)
writeBoulderState(TEST_DIR, baseState)
const stateWithSecond = addBoulderWork(TEST_DIR, {
planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"),
sessionId: "session-b",
})
expect(stateWithSecond).not.toBeNull()
const workIds = Object.keys(stateWithSecond!.works ?? {})
expect(workIds.length).toBe(2)
const firstWorkId = workIds.find((workId) => (stateWithSecond!.works?.[workId]?.plan_name ?? "") === "plan-a")!
const secondWorkId = workIds.find((workId) => (stateWithSecond!.works?.[workId]?.plan_name ?? "") === "plan-b")!
appendSessionIdForWork(TEST_DIR, secondWorkId, "session-a", "appended")
appendSessionIdForWork(TEST_DIR, firstWorkId, "session-a", "appended")
// when
const resolvedWork = getWorkForSession(TEST_DIR, "session-a")
// then
expect(resolvedWork?.work_id).toBe(firstWorkId)
})
test("should support selecting active work and read helpers", () => {
// given
const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a")
writeBoulderState(TEST_DIR, initialState)
const added = addBoulderWork(TEST_DIR, {
planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"),
sessionId: "session-b",
worktreePath: "/tmp/worktree-b",
})
expect(added).not.toBeNull()
const firstWork = getWorkByPlanName(TEST_DIR, "plan-a")
expect(firstWork).not.toBeNull()
// when
const selected = selectActiveWork(TEST_DIR, firstWork!.work_id)
const selectedById = getWorkById(TEST_DIR, firstWork!.work_id)
const byPlanNameWithWorktree = getWorkByPlanName(TEST_DIR, "plan-b", { worktreePath: "/tmp/worktree-b" })
const byPlanPath = resolveBoulderPlanPathForWork(TEST_DIR, firstWork!)
const resumeOptions = getWorkResumeOptions(TEST_DIR)
const worksFromState = getBoulderWorks(selected!)
// then
expect(selected?.active_work_id).toBe(firstWork!.work_id)
expect(selectedById?.work_id).toBe(firstWork!.work_id)
expect(byPlanNameWithWorktree?.plan_name).toBe("plan-b")
expect(byPlanPath.endsWith("plan-a.md")).toBe(true)
expect(resumeOptions.length).toBe(2)
expect(worksFromState.length).toBe(2)
})
test("should upsert task session for specific work and keep first started_at", () => {
// given
const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a")
writeBoulderState(TEST_DIR, initialState)
const workId = initialState.active_work_id!
upsertTaskSessionStateForWork(TEST_DIR, workId, {
taskKey: "todo:1",
taskLabel: "1",
taskTitle: "task one",
sessionId: "task-session-a",
})
const seededState = readBoulderState(TEST_DIR)!
seededState.works![workId]!.task_sessions!["todo:1"]!.started_at = "2026-01-01T00:00:00.000Z"
writeBoulderState(TEST_DIR, seededState)
// when
const updated = upsertTaskSessionStateForWork(TEST_DIR, workId, {
taskKey: "todo:1",
taskLabel: "1",
taskTitle: "task one",
sessionId: "task-session-b",
})
// then
expect(updated).not.toBeNull()
const taskSession = updated?.works?.[workId]?.task_sessions?.["todo:1"]
expect(taskSession?.session_id).toBe("task-session-b")
expect(taskSession?.started_at).toBe("2026-01-01T00:00:00.000Z")
})
})
describe("readCurrentTopLevelTask", () => {
test("should return the first unchecked top-level task in TODOs", () => {
// given - plan with nested and top-level unchecked tasks
+494 -5
View File
@@ -6,11 +6,93 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"
import type { BoulderState, PlanProgress, TaskSessionState } from "./types"
import type {
BoulderSessionOrigin,
BoulderState,
BoulderWorkResumeOption,
BoulderWorkState,
BoulderWorkStatus,
PlanProgress,
TaskSessionState,
} from "./types"
import { BOULDER_DIR, BOULDER_FILE, PROMETHEUS_PLANS_DIR } from "./constants"
const RESERVED_KEYS = new Set(["__proto__", "prototype", "constructor"])
function nowIsoString(): string {
return new Date().toISOString()
}
function parseIsoToMs(value: string | undefined): number | null {
if (!value) {
return null
}
const parsed = Date.parse(value)
return Number.isNaN(parsed) ? null : parsed
}
function isValidWorkStatus(status: unknown): status is BoulderWorkStatus {
return status === "active" || status === "completed" || status === "paused" || status === "abandoned"
}
function buildWorkFromMirror(state: BoulderState): BoulderWorkState {
const planName = state.plan_name ?? getPlanName(state.active_plan)
const workId = `${planName}-legacy`
return {
work_id: workId,
active_plan: state.active_plan,
plan_name: planName,
status: state.status,
started_at: state.started_at,
ended_at: state.ended_at,
elapsed_ms: state.elapsed_ms,
updated_at: state.updated_at,
session_ids: Array.isArray(state.session_ids) ? [...state.session_ids] : [],
session_origins: state.session_origins,
agent: state.agent,
worktree_path: state.worktree_path,
task_sessions: state.task_sessions,
}
}
function projectWorkToMirror(state: BoulderState, work: BoulderWorkState): void {
state.active_plan = work.active_plan
state.plan_name = work.plan_name
state.status = work.status
state.started_at = work.started_at
state.ended_at = work.ended_at
state.elapsed_ms = work.elapsed_ms
state.updated_at = work.updated_at
state.session_ids = [...work.session_ids]
state.session_origins = work.session_origins ? { ...work.session_origins } : {}
state.agent = work.agent
state.worktree_path = work.worktree_path
state.task_sessions = work.task_sessions ? { ...work.task_sessions } : {}
}
function selectMirrorWork(state: BoulderState): BoulderWorkState | null {
const works = getBoulderWorks(state)
if (works.length === 0) {
return null
}
if (state.active_work_id) {
const matched = works.find((work) => work.work_id === state.active_work_id)
if (matched) {
return matched
}
}
const sorted = [...works].sort((left, right) => {
const leftMs = parseIsoToMs(left.updated_at ?? left.started_at) ?? 0
const rightMs = parseIsoToMs(right.updated_at ?? right.started_at) ?? 0
return rightMs - leftMs
})
return sorted[0] ?? null
}
export function getBoulderFilePath(directory: string): string {
return join(directory, BOULDER_DIR, BOULDER_FILE)
}
@@ -80,7 +162,15 @@ export function readBoulderState(directory: string): BoulderState | null {
if (!parsed.task_sessions || typeof parsed.task_sessions !== "object" || Array.isArray(parsed.task_sessions)) {
parsed.task_sessions = {}
}
return parsed as BoulderState
const state = parsed as BoulderState
const mirrorWork = selectMirrorWork(state)
if (mirrorWork) {
state.active_work_id = mirrorWork.work_id
projectWorkToMirror(state, mirrorWork)
}
return state
} catch {
return null
}
@@ -95,7 +185,33 @@ export function writeBoulderState(directory: string, state: BoulderState): boole
mkdirSync(dir, { recursive: true })
}
writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8")
const stateToWrite: BoulderState = { ...state }
if (stateToWrite.works && stateToWrite.active_work_id) {
const activeWork = stateToWrite.works[stateToWrite.active_work_id]
if (activeWork) {
const nextActiveWork: BoulderWorkState = {
...activeWork,
active_plan: stateToWrite.active_plan,
plan_name: stateToWrite.plan_name,
status: stateToWrite.status,
started_at: stateToWrite.started_at,
ended_at: stateToWrite.ended_at,
elapsed_ms: stateToWrite.elapsed_ms,
updated_at: stateToWrite.updated_at,
session_ids: [...stateToWrite.session_ids],
session_origins: stateToWrite.session_origins ? { ...stateToWrite.session_origins } : {},
agent: stateToWrite.agent,
worktree_path: stateToWrite.worktree_path,
task_sessions: stateToWrite.task_sessions ? { ...stateToWrite.task_sessions } : {},
}
stateToWrite.works = {
...stateToWrite.works,
[stateToWrite.active_work_id]: nextActiveWork,
}
}
}
writeFileSync(filePath, JSON.stringify(stateToWrite, null, 2), "utf-8")
return true
} catch {
return false
@@ -107,6 +223,11 @@ export function appendSessionId(
sessionId: string,
origin: "direct" | "appended" = "direct",
): BoulderState | null {
const activeWorkId = readBoulderState(directory)?.active_work_id
if (activeWorkId) {
return appendSessionIdForWork(directory, activeWorkId, sessionId, origin)
}
const state = readBoulderState(directory)
if (!state) return null
@@ -156,6 +277,14 @@ export function clearBoulderState(directory: string): boolean {
export function getTaskSessionState(directory: string, taskKey: string): TaskSessionState | null {
const state = readBoulderState(directory)
if (state?.active_work_id) {
const work = state.works?.[state.active_work_id]
const taskSession = work?.task_sessions?.[taskKey]
if (taskSession) {
return taskSession
}
}
if (!state?.task_sessions) {
return null
}
@@ -174,6 +303,11 @@ export function upsertTaskSessionState(
category?: string
},
): BoulderState | null {
const stateForWork = readBoulderState(directory)
if (stateForWork?.active_work_id) {
return upsertTaskSessionStateForWork(directory, stateForWork.active_work_id, input)
}
const state = readBoulderState(directory)
if (!state) {
return null
@@ -355,15 +489,370 @@ export function createBoulderState(
agent?: string,
worktreePath?: string,
): BoulderState {
return {
const startedAt = nowIsoString()
const workId = generateWorkId(getPlanName(planPath))
const work: BoulderWorkState = {
work_id: workId,
active_plan: planPath,
started_at: new Date().toISOString(),
plan_name: getPlanName(planPath),
status: "active",
started_at: startedAt,
updated_at: startedAt,
session_ids: [sessionId],
session_origins: {
[sessionId]: "direct",
},
...(agent !== undefined ? { agent } : {}),
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
task_sessions: {},
}
return {
schema_version: 2,
active_work_id: workId,
works: {
[workId]: work,
},
active_plan: planPath,
started_at: startedAt,
status: "active",
updated_at: startedAt,
session_ids: [sessionId],
session_origins: {
[sessionId]: "direct",
},
plan_name: getPlanName(planPath),
task_sessions: {},
...(agent !== undefined ? { agent } : {}),
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
}
}
export function generateWorkId(planName: string): string {
const slug = planName
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
const randomHex = Math.floor(Math.random() * 0xffffffff)
.toString(16)
.padStart(8, "0")
const safeSlug = slug.length > 0 ? slug : "work"
return `${safeSlug}-${randomHex}`
}
export function getBoulderWorks(state: BoulderState): BoulderWorkState[] {
if (state.works && typeof state.works === "object") {
return Object.values(state.works)
}
if (!state.active_plan || !state.plan_name || !state.started_at) {
return []
}
return [buildWorkFromMirror(state)]
}
export function getActiveWorks(directory: string): BoulderWorkState[] {
const state = readBoulderState(directory)
if (!state) {
return []
}
return getBoulderWorks(state).filter((work) => work.status !== "completed" && work.status !== "abandoned")
}
export function getWorkById(directory: string, workId: string): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
return getBoulderWorks(state).find((work) => work.work_id === workId) ?? null
}
export function getWorkByPlanName(
directory: string,
planName: string,
options?: { worktreePath?: string },
): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const worktreePath = options?.worktreePath
return getBoulderWorks(state).find((work) => {
if (work.plan_name !== planName) {
return false
}
if (!worktreePath) {
return true
}
return work.worktree_path === worktreePath
}) ?? null
}
export function getWorkForSession(directory: string, sessionId: string): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
.filter((work) => work.session_ids.includes(sessionId))
.sort((left, right) => {
const leftMs = parseIsoToMs(left.updated_at ?? left.started_at) ?? 0
const rightMs = parseIsoToMs(right.updated_at ?? right.started_at) ?? 0
return rightMs - leftMs
})
if (works.length > 0) {
return works[0] ?? null
}
if (state.session_ids.includes(sessionId)) {
return buildWorkFromMirror(state)
}
return null
}
export function resolveBoulderPlanPathForWork(
directory: string,
work: Pick<BoulderWorkState, "active_plan" | "worktree_path">,
): string {
return resolveBoulderPlanPath(directory, work)
}
export function getWorkResumeOptions(directory: string): BoulderWorkResumeOption[] {
const state = readBoulderState(directory)
if (!state) {
return []
}
return getActiveWorks(directory).map((work) => {
const progress = getPlanProgress(resolveBoulderPlanPathForWork(directory, work))
return {
work_id: work.work_id,
plan_name: work.plan_name,
active_plan: work.active_plan,
worktree_path: work.worktree_path,
status: work.status && isValidWorkStatus(work.status) ? work.status : "active",
started_at: work.started_at,
updated_at: work.updated_at ?? work.started_at,
ended_at: work.ended_at,
elapsed_ms: work.elapsed_ms,
session_count: work.session_ids.length,
progress,
is_current_mirror: state.active_work_id === work.work_id,
}
})
}
export function selectActiveWork(directory: string, workId: string): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
const nextWork = works.find((work) => work.work_id === workId)
if (!nextWork) {
return null
}
const nextState: BoulderState = {
...state,
schema_version: 2,
active_work_id: workId,
works: state.works ?? Object.fromEntries(works.map((work) => [work.work_id, work])),
}
projectWorkToMirror(nextState, nextWork)
if (!writeBoulderState(directory, nextState)) {
return null
}
return nextState
}
export function addBoulderWork(
directory: string,
input: {
planPath: string
sessionId: string
agent?: string
worktreePath?: string
startedAt?: string
},
): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const workId = generateWorkId(getPlanName(input.planPath))
const startedAt = input.startedAt ?? nowIsoString()
const nextWork: BoulderWorkState = {
work_id: workId,
active_plan: input.planPath,
plan_name: getPlanName(input.planPath),
status: "active",
started_at: startedAt,
updated_at: startedAt,
session_ids: [input.sessionId],
session_origins: {
[input.sessionId]: "direct",
},
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.worktreePath !== undefined ? { worktree_path: input.worktreePath } : {}),
task_sessions: {},
}
const works = getBoulderWorks(state)
const nextWorks: Record<string, BoulderWorkState> = {
...Object.fromEntries(works.map((work) => [work.work_id, work])),
[workId]: nextWork,
}
const nextState: BoulderState = {
...state,
schema_version: 2,
works: nextWorks,
active_work_id: workId,
}
projectWorkToMirror(nextState, nextWork)
if (!writeBoulderState(directory, nextState)) {
return null
}
return nextState
}
export function appendSessionIdForWork(
directory: string,
workId: string,
sessionId: string,
origin: BoulderSessionOrigin = "direct",
): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
const targetWork = works.find((work) => work.work_id === workId)
if (!targetWork) {
return null
}
const sessionIds = targetWork.session_ids.includes(sessionId)
? [...targetWork.session_ids]
: [...targetWork.session_ids, sessionId]
const sessionOrigins = {
...(targetWork.session_origins ?? {}),
[sessionId]: origin,
}
const updatedWork: BoulderWorkState = {
...targetWork,
session_ids: sessionIds,
session_origins: sessionOrigins,
updated_at: nowIsoString(),
}
const nextWorks = {
...Object.fromEntries(works.map((work) => [work.work_id, work])),
[workId]: updatedWork,
}
const nextState: BoulderState = {
...state,
schema_version: 2,
works: nextWorks,
}
if (state.active_work_id === workId) {
projectWorkToMirror(nextState, updatedWork)
}
if (!writeBoulderState(directory, nextState)) {
return null
}
return nextState
}
export function upsertTaskSessionStateForWork(
directory: string,
workId: string,
input: {
taskKey: string
taskLabel: string
taskTitle: string
sessionId: string
agent?: string
category?: string
},
): BoulderState | null {
if (RESERVED_KEYS.has(input.taskKey)) {
return null
}
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
const targetWork = works.find((work) => work.work_id === workId)
if (!targetWork) {
return null
}
const previousTaskSession = targetWork.task_sessions?.[input.taskKey]
const nextTaskSession: TaskSessionState = {
task_key: input.taskKey,
task_label: input.taskLabel,
task_title: input.taskTitle,
session_id: input.sessionId,
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.category !== undefined ? { category: input.category } : {}),
...(previousTaskSession?.started_at !== undefined ? { started_at: previousTaskSession.started_at } : {}),
...(previousTaskSession?.ended_at !== undefined ? { ended_at: previousTaskSession.ended_at } : {}),
...(previousTaskSession?.elapsed_ms !== undefined ? { elapsed_ms: previousTaskSession.elapsed_ms } : {}),
...(previousTaskSession?.status !== undefined ? { status: previousTaskSession.status } : {}),
updated_at: nowIsoString(),
}
const nextWork: BoulderWorkState = {
...targetWork,
task_sessions: {
...(targetWork.task_sessions ?? {}),
[input.taskKey]: nextTaskSession,
},
updated_at: nowIsoString(),
}
const nextWorks = {
...Object.fromEntries(works.map((work) => [work.work_id, work])),
[workId]: nextWork,
}
const nextState: BoulderState = {
...state,
schema_version: 2,
works: nextWorks,
}
if (state.active_work_id === workId) {
projectWorkToMirror(nextState, nextWork)
}
if (!writeBoulderState(directory, nextState)) {
return null
}
return nextState
}