fix(boulder-state): batch 67 (6 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:11 +09:00
parent 5f13b823cc
commit a2c8b3e31f
6 changed files with 81 additions and 25 deletions
@@ -1,5 +1,6 @@
export { getBoulderFilePath, resolveBoulderPlanPath, resolveBoulderPlanPathForWork } from "./path"
export { findPrometheusPlans, getPlanName, getPlanProgress } from "./plan-progress"
export { normalizeSessionId } from "./shared"
export {
getActiveWorks,
getBoulderWorks,
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs"
import type { BoulderState, BoulderWorkResumeOption, BoulderWorkState, TaskSessionState } from "../types"
import { getBoulderFilePath, resolveBoulderPlanPathForWork } from "./path"
import { getPlanProgress } from "./plan-progress"
import { buildWorkFromMirror, isValidWorkStatus, parseIsoToMs, projectWorkToMirror, selectMirrorWork } from "./shared"
import { buildWorkFromMirror, isValidWorkStatus, normalizeSessionId, parseIsoToMs, projectWorkToMirror, selectMirrorWork } from "./shared"
export function readBoulderState(directory: string): BoulderState | null {
const filePath = getBoulderFilePath(directory)
@@ -33,8 +33,9 @@ export function readBoulderState(directory: string): BoulderState | null {
}
function normalizeState(state: Record<string, unknown>): void {
normalizeSessionFields(state)
const sessionIds = Array.isArray(state.session_ids) ? state.session_ids : []
state.session_ids = sessionIds
const sessionOrigins = state.session_origins && typeof state.session_origins === "object" && !Array.isArray(state.session_origins)
? (state.session_origins as Record<string, unknown>)
@@ -55,6 +56,38 @@ function normalizeState(state: Record<string, unknown>): void {
if (!state.task_sessions || typeof state.task_sessions !== "object" || Array.isArray(state.task_sessions)) {
state.task_sessions = {}
}
normalizeWorkSessionFields(state.works)
}
function normalizeSessionFields(target: Record<string, unknown>): void {
const sessionIds = Array.isArray(target.session_ids)
? target.session_ids.filter((sessionId): sessionId is string => typeof sessionId === "string").map((sessionId) => normalizeSessionId(sessionId))
: []
target.session_ids = sessionIds
const sessionOrigins = target.session_origins && typeof target.session_origins === "object" && !Array.isArray(target.session_origins)
? normalizeSessionOrigins(target.session_origins as Record<string, unknown>)
: {}
target.session_origins = sessionOrigins
}
function normalizeSessionOrigins(sessionOrigins: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(
Object.entries(sessionOrigins).map(([sessionId, origin]) => [normalizeSessionId(sessionId), origin]),
)
}
function normalizeWorkSessionFields(works: unknown): void {
if (!works || typeof works !== "object" || Array.isArray(works)) {
return
}
for (const work of Object.values(works)) {
if (work && typeof work === "object" && !Array.isArray(work)) {
normalizeSessionFields(work as Record<string, unknown>)
}
}
}
export function getBoulderWorks(state: BoulderState): BoulderWorkState[] {
@@ -113,15 +146,16 @@ export function getWorkForSession(directory: string, sessionId: string): Boulder
return null
}
const normalizedSessionId = normalizeSessionId(sessionId)
const works = getBoulderWorks(state)
.filter((work) => work.session_ids.includes(sessionId))
.filter((work) => work.session_ids.includes(normalizedSessionId))
.sort((left, right) => (parseIsoToMs(right.updated_at ?? right.started_at) ?? 0) - (parseIsoToMs(left.updated_at ?? left.started_at) ?? 0))
if (works.length > 0) {
return works[0] ?? null
}
return state.session_ids.includes(sessionId) ? buildWorkFromMirror(state) : null
return state.session_ids.includes(normalizedSessionId) ? buildWorkFromMirror(state) : null
}
export function getWorkResumeOptions(directory: string): BoulderWorkResumeOption[] {
+12 -10
View File
@@ -1,6 +1,6 @@
import type { BoulderSessionOrigin, BoulderState, BoulderWorkState } from "../types"
import { getBoulderWorks, readBoulderState } from "./read-state"
import { nowIsoString, projectWorkToMirror } from "./shared"
import { normalizeSessionId, nowIsoString, projectWorkToMirror } from "./shared"
import { writeBoulderState } from "./write-state"
export function appendSessionId(
@@ -8,9 +8,10 @@ export function appendSessionId(
sessionId: string,
origin: "direct" | "appended" = "direct",
): BoulderState | null {
const normalizedSessionId = normalizeSessionId(sessionId)
const activeWorkId = readBoulderState(directory)?.active_work_id
if (activeWorkId) {
return appendSessionIdForWork(directory, activeWorkId, sessionId, origin)
return appendSessionIdForWork(directory, activeWorkId, normalizedSessionId, origin)
}
const state = readBoulderState(directory)
@@ -22,15 +23,15 @@ export function appendSessionId(
state.session_origins = {}
}
if (!state.session_ids?.includes(sessionId)) {
if (!state.session_ids?.includes(normalizedSessionId)) {
if (!Array.isArray(state.session_ids)) {
state.session_ids = []
}
const originalSessionIds = [...state.session_ids]
const originalSessionOrigins = { ...state.session_origins }
state.session_ids.push(sessionId)
state.session_origins[sessionId] = origin
state.session_ids.push(normalizedSessionId)
state.session_origins[normalizedSessionId] = origin
if (writeBoulderState(directory, state)) {
return state
}
@@ -40,8 +41,8 @@ export function appendSessionId(
return null
}
if (!state.session_origins[sessionId]) {
state.session_origins[sessionId] = origin
if (!state.session_origins[normalizedSessionId]) {
state.session_origins[normalizedSessionId] = origin
if (!writeBoulderState(directory, state)) {
return null
}
@@ -56,6 +57,7 @@ export function appendSessionIdForWork(
sessionId: string,
origin: BoulderSessionOrigin = "direct",
): BoulderState | null {
const normalizedSessionId = normalizeSessionId(sessionId)
const state = readBoulderState(directory)
if (!state) {
return null
@@ -69,10 +71,10 @@ export function appendSessionIdForWork(
const updatedWork: BoulderWorkState = {
...targetWork,
session_ids: targetWork.session_ids.includes(sessionId)
session_ids: targetWork.session_ids.includes(normalizedSessionId)
? [...targetWork.session_ids]
: [...targetWork.session_ids, sessionId],
session_origins: { ...(targetWork.session_origins ?? {}), [sessionId]: origin },
: [...targetWork.session_ids, normalizedSessionId],
session_origins: { ...(targetWork.session_origins ?? {}), [normalizedSessionId]: origin },
updated_at: nowIsoString(),
}
@@ -2,6 +2,18 @@ import type { BoulderState, BoulderWorkState, BoulderWorkStatus } from "../types
export const RESERVED_KEYS = new Set(["__proto__", "prototype", "constructor"])
type SessionPlatform = "codex" | "opencode"
const SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/
export function normalizeSessionId(sessionId: string, platform: SessionPlatform = "opencode"): string {
if (SESSION_ID_PREFIX_PATTERN.test(sessionId)) {
return sessionId
}
return `${platform}:${sessionId}`
}
export function nowIsoString(): string {
return new Date().toISOString()
}
+9 -4
View File
@@ -1,6 +1,6 @@
import type { BoulderState, BoulderWorkState, TaskSessionState } from "../types"
import { getBoulderWorks, readBoulderState } from "./read-state"
import { getElapsedMs, nowIsoString, projectWorkToMirror, RESERVED_KEYS } from "./shared"
import { getElapsedMs, normalizeSessionId, nowIsoString, projectWorkToMirror, RESERVED_KEYS } from "./shared"
import { writeBoulderState } from "./write-state"
export function upsertTaskSessionState(
@@ -24,12 +24,13 @@ export function upsertTaskSessionState(
return null
}
const normalizedSessionId = normalizeSessionId(input.sessionId)
const taskSessions = state.task_sessions ?? {}
taskSessions[input.taskKey] = {
task_key: input.taskKey,
task_label: input.taskLabel,
task_title: input.taskTitle,
session_id: input.sessionId,
session_id: normalizedSessionId,
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.category !== undefined ? { category: input.category } : {}),
updated_at: nowIsoString(),
@@ -66,12 +67,13 @@ export function upsertTaskSessionStateForWork(
return null
}
const normalizedSessionId = normalizeSessionId(input.sessionId)
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,
session_id: normalizedSessionId,
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.category !== undefined ? { category: input.category } : {}),
...(previousTaskSession?.started_at !== undefined ? { started_at: previousTaskSession.started_at } : {}),
@@ -116,7 +118,10 @@ export function startTaskTimer(
startedAt?: string
},
): BoulderState | null {
const nextState = upsertTaskSessionStateForWork(directory, workId, input)
const nextState = upsertTaskSessionStateForWork(directory, workId, {
...input,
sessionId: normalizeSessionId(input.sessionId),
})
if (!nextState) {
return null
}
@@ -5,7 +5,7 @@ import type { BoulderState, BoulderWorkState } from "../types"
import { getBoulderFilePath } from "./path"
import { getPlanName } from "./plan-progress"
import { getBoulderWorks, readBoulderState } from "./read-state"
import { getElapsedMs, nowIsoString, projectWorkToMirror } from "./shared"
import { getElapsedMs, normalizeSessionId, nowIsoString, projectWorkToMirror } from "./shared"
export function writeBoulderState(directory: string, state: BoulderState): boolean {
const filePath = getBoulderFilePath(directory)
@@ -67,6 +67,7 @@ export function generateWorkId(planName: string): string {
export function createBoulderState(planPath: string, sessionId: string, agent?: string, worktreePath?: string): BoulderState {
const startedAt = nowIsoString()
const normalizedSessionId = normalizeSessionId(sessionId)
const workId = generateWorkId(getPlanName(planPath))
const work: BoulderWorkState = {
work_id: workId,
@@ -75,8 +76,8 @@ export function createBoulderState(planPath: string, sessionId: string, agent?:
status: "active",
started_at: startedAt,
updated_at: startedAt,
session_ids: [sessionId],
session_origins: { [sessionId]: "direct" },
session_ids: [normalizedSessionId],
session_origins: { [normalizedSessionId]: "direct" },
...(agent !== undefined ? { agent } : {}),
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
task_sessions: {},
@@ -90,8 +91,8 @@ export function createBoulderState(planPath: string, sessionId: string, agent?:
started_at: startedAt,
status: "active",
updated_at: startedAt,
session_ids: [sessionId],
session_origins: { [sessionId]: "direct" },
session_ids: [normalizedSessionId],
session_origins: { [normalizedSessionId]: "direct" },
plan_name: getPlanName(planPath),
task_sessions: {},
...(agent !== undefined ? { agent } : {}),
@@ -132,6 +133,7 @@ export function addBoulderWork(
const workId = generateWorkId(getPlanName(input.planPath))
const startedAt = input.startedAt ?? nowIsoString()
const normalizedSessionId = normalizeSessionId(input.sessionId)
const nextWork: BoulderWorkState = {
work_id: workId,
active_plan: input.planPath,
@@ -139,8 +141,8 @@ export function addBoulderWork(
status: "active",
started_at: startedAt,
updated_at: startedAt,
session_ids: [input.sessionId],
session_origins: { [input.sessionId]: "direct" },
session_ids: [normalizedSessionId],
session_origins: { [normalizedSessionId]: "direct" },
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.worktreePath !== undefined ? { worktree_path: input.worktreePath } : {}),
task_sessions: {},