@@ -13,6 +13,10 @@ import {
1313 runOpenClawStateWriteTransaction ,
1414} from "../state/openclaw-state-db.js" ;
1515
16+ // Write transactions must run on the same env-scoped handle as their
17+ // statements; a bare transaction would open the default state DB while the
18+ // SQL hits the override, losing atomicity under OPENCLAW_STATE_DIR overrides.
19+
1620export type SessionGroupRecord = { name : string ; position : number } ;
1721
1822type SessionGroupsDatabase = Pick < OpenClawStateKyselyDatabase , "session_groups" > ;
@@ -58,28 +62,30 @@ export function putSessionGroups(
5862 env : NodeJS . ProcessEnv = process . env ,
5963) : SessionGroupRecord [ ] {
6064 const normalized = normalizeGroupNames ( names ) ;
61- const db = dbFor ( env ) ;
62- const kysely = kyselyFor ( db ) ;
6365 const now = Date . now ( ) ;
64- runOpenClawStateWriteTransaction ( ( ) => {
65- const existing = new Map (
66- executeSqliteQuerySync (
67- db ,
68- kysely . selectFrom ( "session_groups" ) . select ( [ "name" , "created_at" ] ) ,
69- ) . rows . map ( ( row ) => [ row . name , row . created_at ] ) ,
70- ) ;
71- executeSqliteQuerySync ( db , kysely . deleteFrom ( "session_groups" ) ) ;
72- normalized . forEach ( ( name , position ) => {
73- executeSqliteQuerySync (
74- db ,
75- kysely . insertInto ( "session_groups" ) . values ( {
76- name,
77- position,
78- created_at : existing . get ( name ) ?? now ,
79- } ) ,
66+ runOpenClawStateWriteTransaction (
67+ ( { db } ) => {
68+ const kysely = kyselyFor ( db ) ;
69+ const existing = new Map (
70+ executeSqliteQuerySync (
71+ db ,
72+ kysely . selectFrom ( "session_groups" ) . select ( [ "name" , "created_at" ] ) ,
73+ ) . rows . map ( ( row ) => [ row . name , row . created_at ] ) ,
8074 ) ;
81- } ) ;
82- } ) ;
75+ executeSqliteQuerySync ( db , kysely . deleteFrom ( "session_groups" ) ) ;
76+ normalized . forEach ( ( name , position ) => {
77+ executeSqliteQuerySync (
78+ db ,
79+ kysely . insertInto ( "session_groups" ) . values ( {
80+ name,
81+ position,
82+ created_at : existing . get ( name ) ?? now ,
83+ } ) ,
84+ ) ;
85+ } ) ;
86+ } ,
87+ { env } ,
88+ ) ;
8389 return normalized . map ( ( name , position ) => ( { name, position } ) ) ;
8490}
8591
@@ -95,57 +101,61 @@ export function ensureSessionGroupRegistered(
95101 if ( ! normalized ) {
96102 return ;
97103 }
98- const db = dbFor ( env ) ;
99- const kysely = kyselyFor ( db ) ;
100- runOpenClawStateWriteTransaction ( ( ) => {
101- const existing = executeSqliteQuerySync (
102- db ,
103- kysely . selectFrom ( "session_groups" ) . select ( "name" ) . where ( "name" , "=" , normalized ) . limit ( 1 ) ,
104- ) . rows [ 0 ] ;
105- if ( existing ) {
106- return ;
107- }
108- const maxRow = executeSqliteQuerySync (
109- db ,
110- kysely . selectFrom ( "session_groups" ) . select ( "position" ) . orderBy ( "position" , "desc" ) . limit ( 1 ) ,
111- ) . rows [ 0 ] ;
112- executeSqliteQuerySync (
113- db ,
114- kysely . insertInto ( "session_groups" ) . values ( {
115- name : normalized ,
116- position : ( maxRow ?. position ?? - 1 ) + 1 ,
117- created_at : Date . now ( ) ,
118- } ) ,
119- ) ;
120- } ) ;
104+ runOpenClawStateWriteTransaction (
105+ ( { db } ) => {
106+ const kysely = kyselyFor ( db ) ;
107+ const existing = executeSqliteQuerySync (
108+ db ,
109+ kysely . selectFrom ( "session_groups" ) . select ( "name" ) . where ( "name" , "=" , normalized ) . limit ( 1 ) ,
110+ ) . rows [ 0 ] ;
111+ if ( existing ) {
112+ return ;
113+ }
114+ const maxRow = executeSqliteQuerySync (
115+ db ,
116+ kysely . selectFrom ( "session_groups" ) . select ( "position" ) . orderBy ( "position" , "desc" ) . limit ( 1 ) ,
117+ ) . rows [ 0 ] ;
118+ executeSqliteQuerySync (
119+ db ,
120+ kysely . insertInto ( "session_groups" ) . values ( {
121+ name : normalized ,
122+ position : ( maxRow ?. position ?? - 1 ) + 1 ,
123+ created_at : Date . now ( ) ,
124+ } ) ,
125+ ) ;
126+ } ,
127+ { env } ,
128+ ) ;
121129}
122130
123131function renameCatalogEntry ( from : string , to : string , env : NodeJS . ProcessEnv ) : void {
124- const db = dbFor ( env ) ;
125- const kysely = kyselyFor ( db ) ;
126- runOpenClawStateWriteTransaction ( ( ) => {
127- const source = executeSqliteQuerySync (
128- db ,
129- kysely . selectFrom ( "session_groups" ) . selectAll ( ) . where ( "name" , "=" , from ) . limit ( 1 ) ,
130- ) . rows [ 0 ] ;
131- const targetExists = executeSqliteQuerySync (
132- db ,
133- kysely . selectFrom ( "session_groups" ) . select ( "name" ) . where ( "name" , "=" , to ) . limit ( 1 ) ,
134- ) . rows [ 0 ] ;
135- executeSqliteQuerySync ( db , kysely . deleteFrom ( "session_groups" ) . where ( "name" , "=" , from ) ) ;
136- if ( targetExists ) {
137- // Rename into an existing group merges memberships; keep the target row.
138- return ;
139- }
140- executeSqliteQuerySync (
141- db ,
142- kysely . insertInto ( "session_groups" ) . values ( {
143- name : to ,
144- position : source ?. position ?? 0 ,
145- created_at : source ?. created_at ?? Date . now ( ) ,
146- } ) ,
147- ) ;
148- } ) ;
132+ runOpenClawStateWriteTransaction (
133+ ( { db } ) => {
134+ const kysely = kyselyFor ( db ) ;
135+ const source = executeSqliteQuerySync (
136+ db ,
137+ kysely . selectFrom ( "session_groups" ) . selectAll ( ) . where ( "name" , "=" , from ) . limit ( 1 ) ,
138+ ) . rows [ 0 ] ;
139+ const targetExists = executeSqliteQuerySync (
140+ db ,
141+ kysely . selectFrom ( "session_groups" ) . select ( "name" ) . where ( "name" , "=" , to ) . limit ( 1 ) ,
142+ ) . rows [ 0 ] ;
143+ executeSqliteQuerySync ( db , kysely . deleteFrom ( "session_groups" ) . where ( "name" , "=" , from ) ) ;
144+ if ( targetExists ) {
145+ // Rename into an existing group merges memberships; keep the target row.
146+ return ;
147+ }
148+ executeSqliteQuerySync (
149+ db ,
150+ kysely . insertInto ( "session_groups" ) . values ( {
151+ name : to ,
152+ position : source ?. position ?? 0 ,
153+ created_at : source ?. created_at ?? Date . now ( ) ,
154+ } ) ,
155+ ) ;
156+ } ,
157+ { env } ,
158+ ) ;
149159}
150160
151161/**
@@ -211,10 +221,15 @@ export async function deleteSessionGroup(params: {
211221 if ( ! name ) {
212222 throw new Error ( "group delete requires a non-empty name" ) ;
213223 }
214- const db = dbFor ( env ) ;
215- runOpenClawStateWriteTransaction ( ( ) => {
216- executeSqliteQuerySync ( db , kyselyFor ( db ) . deleteFrom ( "session_groups" ) . where ( "name" , "=" , name ) ) ;
217- } ) ;
224+ runOpenClawStateWriteTransaction (
225+ ( { db } ) => {
226+ executeSqliteQuerySync (
227+ db ,
228+ kyselyFor ( db ) . deleteFrom ( "session_groups" ) . where ( "name" , "=" , name ) ,
229+ ) ;
230+ } ,
231+ { env } ,
232+ ) ;
218233 const updatedSessions = await updateMemberCategories ( params . cfg , name , undefined , env ) ;
219234 return { groups : listSessionGroups ( env ) , updatedSessions } ;
220235}
0 commit comments