@@ -40,6 +40,7 @@ import type {
4040} from "./types.js" ;
4141
4242const nanoid = customAlphabet ( "abcdefghijklmnopqrstuvwxyz0123456789" , 12 ) ;
43+ const SESSION_STATUS_VALUES_SQL = `'idle', 'starting', 'running', 'failed'` ;
4344
4445// ---------- Row shapes ----------
4546
@@ -158,6 +159,46 @@ function rowToSession(r: SessionRow): Session {
158159 } ;
159160}
160161
162+ function createSessionsTableSql ( tableName : string ) : string {
163+ return `
164+ CREATE TABLE ${ tableName } (
165+ session_id TEXT PRIMARY KEY,
166+ agent_id TEXT NOT NULL,
167+ environment_id TEXT,
168+ status TEXT NOT NULL CHECK (status IN (${ SESSION_STATUS_VALUES_SQL } )),
169+ ephemeral INTEGER NOT NULL DEFAULT 0,
170+ remaining_subagent_depth INTEGER NOT NULL DEFAULT 0,
171+ turns INTEGER NOT NULL DEFAULT 0,
172+ tokens_in INTEGER NOT NULL DEFAULT 0,
173+ tokens_out INTEGER NOT NULL DEFAULT 0,
174+ cost_usd REAL NOT NULL DEFAULT 0,
175+ error TEXT,
176+ created_at INTEGER NOT NULL,
177+ last_event_at INTEGER,
178+ vault_id TEXT,
179+ parent_session_id TEXT,
180+ user_id TEXT
181+ )` ;
182+ }
183+
184+ function createSessionContainersTableSql (
185+ tableName : string ,
186+ sessionsTableName : string ,
187+ ) : string {
188+ return `
189+ CREATE TABLE ${ tableName } (
190+ session_id TEXT PRIMARY KEY REFERENCES ${ sessionsTableName } (session_id) ON DELETE CASCADE,
191+ agent_id TEXT NOT NULL,
192+ container_id TEXT NOT NULL,
193+ container_name TEXT NOT NULL,
194+ container_port INTEGER NOT NULL,
195+ gateway_token TEXT NOT NULL,
196+ claimed_at INTEGER NOT NULL,
197+ boot_ms INTEGER,
198+ pool_source TEXT
199+ )` ;
200+ }
201+
161202// ---------- Schema bootstrap ----------
162203
163204// Applied once per database. Idempotent — every CREATE uses IF NOT EXISTS.
@@ -226,7 +267,8 @@ CREATE TABLE IF NOT EXISTS environments (
226267CREATE TABLE IF NOT EXISTS sessions (
227268 session_id TEXT PRIMARY KEY,
228269 agent_id TEXT NOT NULL,
229- status TEXT NOT NULL CHECK (status IN ('idle', 'running', 'failed')),
270+ environment_id TEXT,
271+ status TEXT NOT NULL CHECK (status IN (${ SESSION_STATUS_VALUES_SQL } )),
230272 ephemeral INTEGER NOT NULL DEFAULT 0,
231273 remaining_subagent_depth INTEGER NOT NULL DEFAULT 0,
232274 turns INTEGER NOT NULL DEFAULT 0,
@@ -237,7 +279,8 @@ CREATE TABLE IF NOT EXISTS sessions (
237279 created_at INTEGER NOT NULL,
238280 last_event_at INTEGER,
239281 vault_id TEXT,
240- parent_session_id TEXT
282+ parent_session_id TEXT,
283+ user_id TEXT
241284);
242285
243286CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);
@@ -1794,6 +1837,7 @@ export class SqliteStore implements Store {
17941837 if ( scCols . length > 0 && ! scCols . some ( ( c ) => c . name === "pool_source" ) ) {
17951838 this . db . exec ( "ALTER TABLE session_containers ADD COLUMN pool_source TEXT" ) ;
17961839 }
1840+ this . migrateSessionStatusConstraint ( ) ;
17971841
17981842 const envCols = this . db . pragma ( "table_info(environments)" ) as Array < {
17991843 name : string ;
@@ -1845,6 +1889,86 @@ export class SqliteStore implements Store {
18451889 this . migratedVaultCredentials = vaultsStore . migratePlaintextCredentials ( ) ;
18461890 }
18471891
1892+ private migrateSessionStatusConstraint ( ) : void {
1893+ const row = this . db
1894+ . prepare (
1895+ `SELECT sql
1896+ FROM sqlite_master
1897+ WHERE type = 'table' AND name = 'sessions'` ,
1898+ )
1899+ . get ( ) as { sql : string | null } | undefined ;
1900+ const sessionsSql = row ?. sql ?? "" ;
1901+ if ( sessionsSql . includes ( "'starting'" ) ) return ;
1902+
1903+ const fkWasEnabled =
1904+ ( this . db . pragma ( "foreign_keys" , { simple : true } ) as number ) === 1 ;
1905+ if ( fkWasEnabled ) {
1906+ this . db . pragma ( "foreign_keys = OFF" ) ;
1907+ }
1908+ try {
1909+ const migrate = this . db . transaction ( ( ) => {
1910+ this . db . exec ( createSessionsTableSql ( "sessions_new" ) ) ;
1911+ this . db . exec ( `
1912+ INSERT INTO sessions_new (
1913+ session_id, agent_id, environment_id, status, ephemeral,
1914+ remaining_subagent_depth, turns, tokens_in, tokens_out, cost_usd,
1915+ error, created_at, last_event_at, vault_id, parent_session_id, user_id
1916+ )
1917+ SELECT
1918+ session_id, agent_id, environment_id, status, ephemeral,
1919+ remaining_subagent_depth, turns, tokens_in, tokens_out, cost_usd,
1920+ error, created_at, last_event_at, vault_id, parent_session_id, user_id
1921+ FROM sessions
1922+ ` ) ;
1923+ this . db . exec (
1924+ createSessionContainersTableSql (
1925+ "session_containers_new" ,
1926+ "sessions_new" ,
1927+ ) ,
1928+ ) ;
1929+ this . db . exec ( `
1930+ INSERT INTO session_containers_new (
1931+ session_id, agent_id, container_id, container_name, container_port,
1932+ gateway_token, claimed_at, boot_ms, pool_source
1933+ )
1934+ SELECT
1935+ session_id, agent_id, container_id, container_name, container_port,
1936+ gateway_token, claimed_at, boot_ms, pool_source
1937+ FROM session_containers
1938+ ` ) ;
1939+ this . db . exec ( "DROP TABLE session_containers" ) ;
1940+ this . db . exec ( "DROP TABLE sessions" ) ;
1941+ this . db . exec ( "ALTER TABLE sessions_new RENAME TO sessions" ) ;
1942+ this . db . exec (
1943+ "ALTER TABLE session_containers_new RENAME TO session_containers" ,
1944+ ) ;
1945+ this . db . exec (
1946+ "CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id)" ,
1947+ ) ;
1948+ this . db . exec (
1949+ "CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)" ,
1950+ ) ;
1951+ this . db . exec (
1952+ "CREATE INDEX IF NOT EXISTS idx_session_containers_container ON session_containers(container_id)" ,
1953+ ) ;
1954+ } ) ;
1955+ migrate ( ) ;
1956+ } finally {
1957+ if ( fkWasEnabled ) {
1958+ this . db . pragma ( "foreign_keys = ON" ) ;
1959+ }
1960+ }
1961+
1962+ const fkViolations = this . db . pragma (
1963+ "foreign_key_check" ,
1964+ ) as Array < Record < string , unknown > > ;
1965+ if ( fkViolations . length > 0 ) {
1966+ throw new Error (
1967+ `session status migration left foreign key violations: ${ JSON . stringify ( fkViolations ) } ` ,
1968+ ) ;
1969+ }
1970+ }
1971+
18481972 close ( ) : void {
18491973 if ( this . closed ) return ;
18501974 this . db . close ( ) ;
0 commit comments