@@ -5,6 +5,88 @@ import path from "node:path";
55
66const STORE_VERSION = 1 ;
77
8+ const LIFECYCLE_VALUES = new Set ( [ "active" , "inactive" , "completed" , "blocked" ] ) ;
9+
10+ function normalizeLifecycleStatus ( value ) {
11+ if ( value === "cleaned" ) return "completed" ;
12+ return LIFECYCLE_VALUES . has ( value ) ? value : "inactive" ;
13+ }
14+
15+ function createDefaultState ( repoRoot , sessionID ) {
16+ return {
17+ schema_version : STORE_VERSION ,
18+ repo_root : path . resolve ( repoRoot ) ,
19+ session_id : sessionID ,
20+ active_task_id : null ,
21+ tasks : [ ] ,
22+ } ;
23+ }
24+
25+ function findTaskIndex ( tasks , taskPatch ) {
26+ if ( taskPatch ?. task_id ) {
27+ const byID = tasks . findIndex ( ( task ) => task ?. task_id === taskPatch . task_id ) ;
28+ if ( byID !== - 1 ) return byID ;
29+ }
30+
31+ return tasks . findIndex ( ( task ) => {
32+ if ( taskPatch ?. branch && task ?. branch === taskPatch . branch ) return true ;
33+ if ( taskPatch ?. worktree_path && task ?. worktree_path === taskPatch . worktree_path ) return true ;
34+ return false ;
35+ } ) ;
36+ }
37+
38+ function normalizeLoadedState ( parsed , repoRoot , sessionID ) {
39+ if ( ! parsed || typeof parsed !== "object" ) return createDefaultState ( repoRoot , sessionID ) ;
40+
41+ const state = createDefaultState ( repoRoot , sessionID ) ;
42+ const incomingTasks = Array . isArray ( parsed . tasks ) ? parsed . tasks : [ ] ;
43+ const legacyActiveTask = typeof parsed . active_task === "string" && parsed . active_task ? parsed . active_task : null ;
44+ const declaredActiveTask = typeof parsed . active_task_id === "string" && parsed . active_task_id ? parsed . active_task_id : null ;
45+
46+ state . tasks = incomingTasks . map ( ( task , index ) => {
47+ const normalized = {
48+ ...( task && typeof task === "object" ? task : { } ) ,
49+ task_id : task ?. task_id || task ?. branch || task ?. worktree_path || `task-${ index + 1 } ` ,
50+ status : normalizeLifecycleStatus ( task ?. status ) ,
51+ } ;
52+ return normalized ;
53+ } ) ;
54+
55+ let activeTaskID = declaredActiveTask ;
56+ if ( ! activeTaskID && legacyActiveTask ) {
57+ const legacyMatch = state . tasks . find ( ( task ) => task . task_id === legacyActiveTask || task . branch === legacyActiveTask ) ;
58+ activeTaskID = legacyMatch ?. task_id ?? null ;
59+ }
60+ if ( ! activeTaskID ) {
61+ const activeFromStatus = state . tasks . find ( ( task ) => task . status === "active" ) ;
62+ activeTaskID = activeFromStatus ?. task_id ?? null ;
63+ }
64+ if ( activeTaskID ) {
65+ const activeTask = state . tasks . find ( ( task ) => task . task_id === activeTaskID ) ;
66+ if ( ! activeTask || activeTask . status === "completed" || activeTask . status === "blocked" ) {
67+ activeTaskID = null ;
68+ }
69+ }
70+
71+ state . active_task_id = activeTaskID ;
72+ if ( activeTaskID ) {
73+ state . tasks = state . tasks . map ( ( task ) => {
74+ if ( task . status === "completed" || task . status === "blocked" ) return task ;
75+ return {
76+ ...task ,
77+ status : task . task_id === activeTaskID ? "active" : "inactive" ,
78+ } ;
79+ } ) ;
80+ } else {
81+ state . tasks = state . tasks . map ( ( task ) => {
82+ if ( task . status === "active" ) return { ...task , status : "inactive" } ;
83+ return task ;
84+ } ) ;
85+ }
86+
87+ return state ;
88+ }
89+
890function defaultStateDir ( env = process . env , platform = process . platform ) {
991 if ( env . OPENCODE_WORKTREE_STATE_DIR ) {
1092 return path . resolve ( env . OPENCODE_WORKTREE_STATE_DIR ) ;
@@ -44,13 +126,10 @@ export function createRuntimeStateStore({ stateDir = defaultStateDir(), now = ()
44126 try {
45127 const raw = await fs . readFile ( filePath , "utf8" ) ;
46128 const parsed = JSON . parse ( raw ) ;
47- if ( ! parsed || typeof parsed !== "object" ) {
48- return { schema_version : STORE_VERSION , repo_root : path . resolve ( repoRoot ) , session_id : sessionID , active_task : null , tasks : [ ] } ;
49- }
50- return parsed ;
129+ return normalizeLoadedState ( parsed , repoRoot , sessionID ) ;
51130 } catch ( error ) {
52131 if ( error && typeof error === "object" && "code" in error && error . code === "ENOENT" ) {
53- return { schema_version : STORE_VERSION , repo_root : path . resolve ( repoRoot ) , session_id : sessionID , active_task : null , tasks : [ ] } ;
132+ return createDefaultState ( repoRoot , sessionID ) ;
54133 }
55134 throw error ;
56135 }
@@ -63,24 +142,40 @@ export function createRuntimeStateStore({ stateDir = defaultStateDir(), now = ()
63142 }
64143
65144 function getActiveTask ( state ) {
66- return state ?. active_task ?? null ;
145+ return state ?. active_task_id ?? null ;
67146 }
68147
69- function setActiveTask ( state , activeTask ) {
148+ function setActiveTask ( state , activeTaskID ) {
149+ const tasks = Array . isArray ( state ?. tasks ) ? state . tasks : [ ] ;
150+ const nextTasks = tasks . map ( ( task ) => {
151+ if ( ! task || typeof task !== "object" ) return task ;
152+ if ( task . status === "completed" || task . status === "blocked" ) return task ;
153+ if ( ! activeTaskID ) {
154+ return task . status === "active" ? { ...task , status : "inactive" } : task ;
155+ }
156+ return {
157+ ...task ,
158+ status : task . task_id === activeTaskID ? "active" : "inactive" ,
159+ } ;
160+ } ) ;
161+
70162 return {
71163 ...state ,
72- active_task : activeTask ,
164+ active_task_id : activeTaskID ,
165+ tasks : nextTasks ,
73166 } ;
74167 }
75168
76169 function upsertTask ( state , taskPatch ) {
77170 const timestamp = now ( ) ;
78171 const tasks = Array . isArray ( state ?. tasks ) ? [ ...state . tasks ] : [ ] ;
79- const index = tasks . findIndex ( ( task ) => task ?. branch === taskPatch ?. branch || task ?. worktree_path === taskPatch ?. worktree_path ) ;
172+ const index = findTaskIndex ( tasks , taskPatch ) ;
80173
81174 if ( index === - 1 ) {
82175 tasks . push ( {
83176 ...taskPatch ,
177+ task_id : taskPatch ?. task_id || taskPatch ?. branch || taskPatch ?. worktree_path || `task-${ tasks . length + 1 } ` ,
178+ status : normalizeLifecycleStatus ( taskPatch ?. status ) ,
84179 created_at : timestamp ,
85180 last_used_at : timestamp ,
86181 } ) ;
@@ -89,6 +184,8 @@ export function createRuntimeStateStore({ stateDir = defaultStateDir(), now = ()
89184 tasks [ index ] = {
90185 ...existing ,
91186 ...taskPatch ,
187+ task_id : existing . task_id || taskPatch ?. task_id || taskPatch ?. branch || taskPatch ?. worktree_path || `task-${ index + 1 } ` ,
188+ status : normalizeLifecycleStatus ( taskPatch ?. status ?? existing . status ) ,
92189 created_at : existing . created_at || timestamp ,
93190 last_used_at : timestamp ,
94191 } ;
0 commit comments