From ec49bd553fc0cba46d1e3ce4617d5b0a67c4a852 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 5 Apr 2026 17:14:26 +0900 Subject: [PATCH] feat(boulder-state): add session_origins tracking for direct vs appended sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/features/boulder-state/storage.test.ts | 66 ++++++++++++++++++++++ src/features/boulder-state/storage.ts | 36 +++++++++++- src/features/boulder-state/types.ts | 1 + 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/features/boulder-state/storage.test.ts b/src/features/boulder-state/storage.test.ts index f391b80fd..0b2abe4d6 100644 --- a/src/features/boulder-state/storage.test.ts +++ b/src/features/boulder-state/storage.test.ts @@ -118,6 +118,40 @@ describe("boulder-state", () => { 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", () => { // given - valid boulder.json const state: BoulderState = { @@ -239,6 +273,26 @@ describe("boulder-state", () => { expect(result).not.toBeNull() 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", () => { @@ -547,6 +601,18 @@ describe("boulder-state", () => { 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", () => { //#given - plan path and session id without agent const planPath = "/path/to/legacy.md" diff --git a/src/features/boulder-state/storage.ts b/src/features/boulder-state/storage.ts index 0bef67bff..0b3eefe54 100644 --- a/src/features/boulder-state/storage.ts +++ b/src/features/boulder-state/storage.ts @@ -31,6 +31,19 @@ export function readBoulderState(directory: string): BoulderState | null { if (!Array.isArray(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)) { 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) 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 (!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 if (writeBoulderState(directory, state)) { return state } state.session_ids = originalSessionIds + state.session_origins = originalSessionOrigins return null } + if (!state.session_origins[sessionId]) { + state.session_origins[sessionId] = origin + if (!writeBoulderState(directory, state)) { + return null + } + } + return state } @@ -213,6 +244,9 @@ export function createBoulderState( active_plan: planPath, started_at: new Date().toISOString(), session_ids: [sessionId], + session_origins: { + [sessionId]: "direct", + }, plan_name: getPlanName(planPath), ...(agent !== undefined ? { agent } : {}), ...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}), diff --git a/src/features/boulder-state/types.ts b/src/features/boulder-state/types.ts index ba488f381..f41bc1bf8 100644 --- a/src/features/boulder-state/types.ts +++ b/src/features/boulder-state/types.ts @@ -12,6 +12,7 @@ export interface BoulderState { started_at: string /** Session IDs that have worked on this plan */ session_ids: string[] + session_origins?: Record /** Plan name derived from filename */ plan_name: string /** Agent type to use when resuming (e.g., 'atlas') */