From a2c8b3e31f250a6f70c91cfe499fac8c8f70ef97 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 30 May 2026 19:12:11 +0900 Subject: [PATCH] fix(boulder-state): batch 67 (6 files) --- packages/boulder-state/src/storage/index.ts | 1 + .../boulder-state/src/storage/read-state.ts | 42 +++++++++++++++++-- packages/boulder-state/src/storage/session.ts | 22 +++++----- packages/boulder-state/src/storage/shared.ts | 12 ++++++ packages/boulder-state/src/storage/task.ts | 13 ++++-- .../boulder-state/src/storage/write-state.ts | 16 +++---- 6 files changed, 81 insertions(+), 25 deletions(-) diff --git a/packages/boulder-state/src/storage/index.ts b/packages/boulder-state/src/storage/index.ts index c17fbe2d6..057ad534c 100644 --- a/packages/boulder-state/src/storage/index.ts +++ b/packages/boulder-state/src/storage/index.ts @@ -1,5 +1,6 @@ export { getBoulderFilePath, resolveBoulderPlanPath, resolveBoulderPlanPathForWork } from "./path" export { findPrometheusPlans, getPlanName, getPlanProgress } from "./plan-progress" +export { normalizeSessionId } from "./shared" export { getActiveWorks, getBoulderWorks, diff --git a/packages/boulder-state/src/storage/read-state.ts b/packages/boulder-state/src/storage/read-state.ts index ee5d3f60d..a68268003 100644 --- a/packages/boulder-state/src/storage/read-state.ts +++ b/packages/boulder-state/src/storage/read-state.ts @@ -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): 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) @@ -55,6 +56,38 @@ function normalizeState(state: Record): 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): 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) + : {} + target.session_origins = sessionOrigins +} + +function normalizeSessionOrigins(sessionOrigins: Record): Record { + 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) + } + } } 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[] { diff --git a/packages/boulder-state/src/storage/session.ts b/packages/boulder-state/src/storage/session.ts index abecb8b21..12f4755e9 100644 --- a/packages/boulder-state/src/storage/session.ts +++ b/packages/boulder-state/src/storage/session.ts @@ -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(), } diff --git a/packages/boulder-state/src/storage/shared.ts b/packages/boulder-state/src/storage/shared.ts index 50fd89e7b..00c1811ad 100644 --- a/packages/boulder-state/src/storage/shared.ts +++ b/packages/boulder-state/src/storage/shared.ts @@ -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() } diff --git a/packages/boulder-state/src/storage/task.ts b/packages/boulder-state/src/storage/task.ts index c17fedd9b..921b6b7a4 100644 --- a/packages/boulder-state/src/storage/task.ts +++ b/packages/boulder-state/src/storage/task.ts @@ -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 } diff --git a/packages/boulder-state/src/storage/write-state.ts b/packages/boulder-state/src/storage/write-state.ts index 1360b43ad..5cabd8841 100644 --- a/packages/boulder-state/src/storage/write-state.ts +++ b/packages/boulder-state/src/storage/write-state.ts @@ -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: {},