@@ -21,46 +21,63 @@ const defaultConfig = {
2121 } ,
2222 models : { } ,
2323 workspace : '~/.openclaw/workspace' ,
24+ imageMaxDimensionPx : 1200 ,
25+ compaction : {
26+ mode : 'default' ,
27+ } ,
2428 heartbeat : {
2529 every : '30m' ,
26- model : '' ,
2730 suppressToolErrorWarnings : false ,
2831 } ,
2932 } ,
3033 list : [ ] ,
3134 } ,
35+ messages : {
36+ queue : {
37+ mode : 'collect' ,
38+ } ,
39+ } ,
3240 channels : {
33- whatsapp : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' , allowFrom : [ ] } ,
34- telegram : { enabled : false , botToken : '' , dmPolicy : 'pairing' , groupPolicy : 'allowlist' , allowFrom : [ ] } ,
35- discord : { enabled : false , botToken : '' , applicationId : '' , dmPolicy : 'pairing' , groupPolicy : 'allowlist' , allowFrom : [ ] } ,
36- slack : { enabled : false , botToken : '' , appToken : '' , dmPolicy : 'pairing' , groupPolicy : 'allowlist' , allowFrom : [ ] } ,
37- signal : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
38- imessage : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
39- googlechat : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
40- mattermost : { enabled : false , url : '' , token : '' , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
41- msteams : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
42- bluebubbles : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
43- matrix : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
44- zalo : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
45- webchat : { enabled : false , dmPolicy : 'pairing' , groupPolicy : 'allowlist' } ,
41+ whatsapp : { enabled : false , dmPolicy : 'pairing' , allowFrom : [ ] } ,
42+ telegram : { enabled : false , botToken : '' , dmPolicy : 'pairing' , allowFrom : [ ] } ,
43+ discord : { enabled : false , botToken : '' , applicationId : '' , dmPolicy : 'pairing' , allowFrom : [ ] } ,
44+ slack : { enabled : false , botToken : '' , appToken : '' , dmPolicy : 'pairing' , allowFrom : [ ] } ,
45+ signal : { enabled : false , dmPolicy : 'pairing' } ,
46+ imessage : { enabled : false , dmPolicy : 'pairing' } ,
47+ googlechat : { enabled : false , dmPolicy : 'pairing' } ,
48+ mattermost : { enabled : false , url : '' , token : '' , dmPolicy : 'pairing' } ,
49+ msteams : { enabled : false , dmPolicy : 'pairing' } ,
50+ bluebubbles : { enabled : false , dmPolicy : 'pairing' } ,
51+ matrix : { enabled : false , dmPolicy : 'pairing' } ,
52+ zalo : { enabled : false , dmPolicy : 'pairing' } ,
53+ webchat : { enabled : false , dmPolicy : 'pairing' } ,
4654 } ,
4755 gateway : {
56+ mode : 'local' ,
4857 port : 18789 ,
49- bind : '127.0.0.1 ' ,
58+ bind : 'loopback ' ,
5059 auth : {
5160 mode : 'token' ,
52- token : '' ,
5361 } ,
5462 tailscale : {
5563 mode : 'off' ,
5664 } ,
5765 } ,
66+ session : {
67+ dmScope : 'main' ,
68+ } ,
69+ browser : {
70+ enabled : true ,
71+ defaultProfile : 'chrome' ,
72+ headless : false ,
73+ ssrfPolicy : {
74+ dangerouslyAllowPrivateNetwork : true ,
75+ } ,
76+ } ,
5877 env : { } ,
5978 tools : {
60- disabled : [ ] ,
79+ deny : [ ] ,
6180 } ,
62- sessionRetention : '7d' ,
63- imageMaxDimensionPx : 1024 ,
6481}
6582
6683const initialState = {
@@ -70,6 +87,16 @@ const initialState = {
7087 selectedTemplate : null ,
7188 config : { ...defaultConfig } ,
7289 soulMd : '' ,
90+ workspaceFiles : {
91+ 'AGENTS.md' : '' ,
92+ 'BOOT.md' : '' ,
93+ 'BOOTSTRAP.md' : '' ,
94+ 'HEARTBEAT.md' : '' ,
95+ 'IDENTITY' : '' ,
96+ 'SOUL.md' : '' ,
97+ 'TOOLS.md' : '' ,
98+ 'USER' : '' ,
99+ } ,
73100 provider : 'anthropic' ,
74101 apiKey : '' ,
75102 skippedFields : [ ] ,
@@ -80,7 +107,7 @@ function loadState() {
80107 const saved = localStorage . getItem ( STORAGE_KEY )
81108 if ( saved ) {
82109 const parsed = JSON . parse ( saved )
83- return { ... initialState , ... parsed }
110+ return deepMerge ( initialState , parsed )
84111 }
85112 } catch ( e ) { /* ignore */ }
86113 return initialState
@@ -104,7 +131,24 @@ function wizardReducer(state, action) {
104131 case 'SET_API_KEY' :
105132 return { ...state , apiKey : action . payload }
106133 case 'SET_SOUL_MD' :
107- return { ...state , soulMd : action . payload }
134+ return {
135+ ...state ,
136+ soulMd : action . payload ,
137+ workspaceFiles : { ...state . workspaceFiles , 'SOUL.md' : action . payload }
138+ }
139+ case 'SET_WORKSPACE_FILE' :
140+ return {
141+ ...state ,
142+ workspaceFiles : { ...state . workspaceFiles , [ action . payload . name ] : action . payload . content } ,
143+ // Keep soulMd in sync if SOUL.md is edited
144+ soulMd : action . payload . name === 'SOUL.md' ? action . payload . content : state . soulMd
145+ }
146+ case 'SET_ALL_WORKSPACE_FILES' :
147+ return {
148+ ...state ,
149+ workspaceFiles : { ...state . workspaceFiles , ...action . payload } ,
150+ soulMd : action . payload [ 'SOUL.md' ] || state . soulMd
151+ }
108152 case 'UPDATE_CONFIG' :
109153 return { ...state , config : deepMerge ( state . config , action . payload ) }
110154 case 'SET_CHANNEL' : {
@@ -128,16 +172,6 @@ function wizardReducer(state, action) {
128172 gateway : { ...state . config . gateway , ...action . payload } ,
129173 } ,
130174 }
131- case 'TOGGLE_TOOL_DISABLED' : {
132- const tool = action . payload
133- const disabled = state . config . tools . disabled . includes ( tool )
134- ? state . config . tools . disabled . filter ( t => t !== tool )
135- : [ ...state . config . tools . disabled , tool ]
136- return {
137- ...state ,
138- config : { ...state . config , tools : { ...state . config . tools , disabled } } ,
139- }
140- }
141175 case 'SKIP_FIELD' : {
142176 const set = new Set ( state . skippedFields )
143177 set . add ( action . payload )
@@ -200,31 +234,98 @@ export function WizardProvider({ children }) {
200234 }
201235
202236 const generateConfig = ( ) => {
203- const cfg = { ...state . config }
237+ const cfg = JSON . parse ( JSON . stringify ( state . config ) )
238+
239+ // ═══ SCHEMA MIGRATION — fix old localStorage values ═══
204240
205- // Set model
241+ // 1. compaction: must be object { mode: "default"|"safeguard" }, not boolean
242+ if ( cfg . agents ?. defaults ) {
243+ if ( typeof cfg . agents . defaults . compaction === 'boolean' || cfg . agents . defaults . compaction === undefined ) {
244+ cfg . agents . defaults . compaction = { mode : 'default' }
245+ }
246+ // Remove keys not in official schema
247+ delete cfg . agents . defaults . timeoutSeconds
248+ delete cfg . agents . defaults . blockStreamingDefault
249+ // heartbeat.model empty string is invalid — remove it
250+ if ( cfg . agents . defaults . heartbeat ?. model === '' ) {
251+ delete cfg . agents . defaults . heartbeat . model
252+ }
253+ }
254+
255+ // 2. tools: "disabled" key is not valid — migrate to "deny"
256+ if ( cfg . tools ) {
257+ if ( cfg . tools . disabled ) {
258+ cfg . tools . deny = cfg . tools . disabled
259+ delete cfg . tools . disabled
260+ }
261+ }
262+
263+ // 3. session.dmScope: valid values are "main"|"per-peer"|"per-channel-peer"|"per-account-channel-peer"
264+ const validDmScopes = [ 'main' , 'per-peer' , 'per-channel-peer' , 'per-account-channel-peer' ]
265+ if ( cfg . session && ! validDmScopes . includes ( cfg . session . dmScope ) ) {
266+ cfg . session . dmScope = 'main'
267+ }
268+
269+ // 4. gateway.bind: valid values are "loopback"|"lan"|"tailnet"|"auto" (not IP addresses)
270+ const validBinds = [ 'loopback' , 'lan' , 'tailnet' , 'auto' ]
271+ if ( cfg . gateway && ! validBinds . includes ( cfg . gateway . bind ) ) {
272+ // Map old IP values to correct enum
273+ if ( cfg . gateway . bind === '127.0.0.1' || cfg . gateway . bind === 'localhost' ) {
274+ cfg . gateway . bind = 'loopback'
275+ } else if ( cfg . gateway . bind === '0.0.0.0' ) {
276+ cfg . gateway . bind = 'lan'
277+ } else {
278+ cfg . gateway . bind = 'loopback'
279+ }
280+ }
281+ // Remove empty auth.token (auto-generated by openclaw)
282+ if ( cfg . gateway ?. auth ?. token === '' ) {
283+ delete cfg . gateway . auth . token
284+ }
285+
286+ // 5. Remove invalid root-level keys
287+ delete cfg . sessionRetention
288+ delete cfg . imageMaxDimensionPx
289+
290+ // 6. Remove groupPolicy from channels (not a valid key)
291+ for ( const ch of Object . values ( cfg . channels || { } ) ) {
292+ delete ch . groupPolicy
293+ }
294+
295+ // 7. Remove extra queue keys that aren't in schema
296+ if ( cfg . messages ?. queue ) {
297+ delete cfg . messages . queue . debounceMs
298+ delete cfg . messages . queue . cap
299+ delete cfg . messages . queue . drop
300+ }
301+
302+ // ═══ END MIGRATION ═══
303+
304+ // Set provider API key in env
206305 if ( state . apiKey && state . provider ) {
207306 const envKey = {
208307 anthropic : 'ANTHROPIC_API_KEY' ,
209308 openai : 'OPENAI_API_KEY' ,
210309 openrouter : 'OPENROUTER_API_KEY' ,
211- google : 'GOOGLE_API_KEY ' ,
212- kilocode : 'KILOCODE_API_KEY ' ,
310+ google : 'GEMINI_API_KEY ' ,
311+ kilocode : 'KILO_API_KEY ' ,
213312 groq : 'GROQ_API_KEY' ,
214313 mistral : 'MISTRAL_API_KEY' ,
215314 xai : 'XAI_API_KEY' ,
216315 together : 'TOGETHER_API_KEY' ,
217316 deepseek : 'DEEPSEEK_API_KEY' ,
218- cerebras : 'CEREBRAS_API_KEY' ,
219317 moonshot : 'MOONSHOT_API_KEY' ,
220318 minimax : 'MINIMAX_API_KEY' ,
319+ venice : 'VENICE_API_KEY' ,
320+ opencode : 'OPENCODE_API_KEY' ,
321+ synthetic : 'SYNTHETIC_API_KEY' ,
221322 } [ state . provider ]
222323 if ( envKey ) {
223324 cfg . env = { ...cfg . env , [ envKey ] : state . apiKey }
224325 }
225326 }
226327
227- // Remove disabled channels
328+ // Remove disabled channels (keep only enabled ones without the 'enabled' flag)
228329 const channels = { }
229330 for ( const [ key , val ] of Object . entries ( cfg . channels ) ) {
230331 if ( val . enabled ) {
@@ -234,6 +335,34 @@ export function WizardProvider({ children }) {
234335 }
235336 cfg . channels = channels
236337
338+ // Clean up tools: remove empty deny array
339+ if ( cfg . tools ?. deny ?. length === 0 ) {
340+ delete cfg . tools . deny
341+ }
342+ if ( cfg . tools && Object . keys ( cfg . tools ) . length === 0 ) {
343+ delete cfg . tools
344+ }
345+
346+ // Clean up browser: remove internal 'enabled' flag
347+ if ( cfg . browser ) {
348+ delete cfg . browser . enabled
349+ }
350+
351+ // Clean up env: remove if empty
352+ if ( cfg . env && Object . keys ( cfg . env ) . length === 0 ) {
353+ delete cfg . env
354+ }
355+
356+ // Clean up empty agents.list
357+ if ( cfg . agents ?. list ?. length === 0 ) {
358+ delete cfg . agents . list
359+ }
360+
361+ // Clean up empty models
362+ if ( cfg . agents ?. defaults ?. models && Object . keys ( cfg . agents . defaults . models ) . length === 0 ) {
363+ delete cfg . agents . defaults . models
364+ }
365+
237366 return cfg
238367 }
239368
0 commit comments