* add gated codex goals support * sync README SDK versions * bump claude agent sdk * add codex goals settings toggle * reuse status dashboard message and simplify settings actions * refine settings page UX * fix settings nav hash routing * add dashboard UX verification * refine dashboard settings and inbox UX * remove inbox top-level navigation * remove dashboard health top-level navigation * fix: allow runtime image attachment paths * feat: configure attachment allowlist dirs * fix: clean duplicate dashboard status messages * fix: poll dashboard duplicate cleanup between status updates * fix: delete dashboard duplicates on create * Refine settings IA with tabbed sections * Add read-only runtime inventory settings * Fix runtime inventory MCP JSON parsing * Add room skill settings inventory * Add room skill setting mutations * Apply room skill overrides to runner spawn * Refine scheduled task dashboard UX * Remove reviewer final prompt reinjection * Stabilize pnpm verification fixture * Remove previous owner final prompt carryover * Clear role-scoped sessions
169 lines
4.0 KiB
TypeScript
169 lines
4.0 KiB
TypeScript
import { Database } from 'bun:sqlite';
|
|
|
|
import { AgentType } from '../types.js';
|
|
|
|
export type ServiceShadowAgentTypeResolver = (
|
|
serviceId: string,
|
|
) => AgentType | undefined;
|
|
|
|
function hasLegacyServiceSessionsTable(database: Database): boolean {
|
|
return Boolean(
|
|
database
|
|
.prepare(
|
|
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'service_sessions'`,
|
|
)
|
|
.get(),
|
|
);
|
|
}
|
|
|
|
export function migrateSessionsTableToCompositePk(
|
|
database: Database,
|
|
defaultAgentType: AgentType,
|
|
): void {
|
|
const sessionCols = database
|
|
.prepare('PRAGMA table_info(sessions)')
|
|
.all() as Array<{ name: string }>;
|
|
if (sessionCols.some((col) => col.name === 'agent_type')) {
|
|
return;
|
|
}
|
|
|
|
database.exec(`
|
|
CREATE TABLE sessions_new (
|
|
group_folder TEXT NOT NULL,
|
|
agent_type TEXT NOT NULL DEFAULT '${defaultAgentType}',
|
|
session_id TEXT NOT NULL,
|
|
PRIMARY KEY (group_folder, agent_type)
|
|
);
|
|
`);
|
|
database
|
|
.prepare(
|
|
`INSERT INTO sessions_new (group_folder, agent_type, session_id)
|
|
SELECT group_folder, ?, session_id FROM sessions`,
|
|
)
|
|
.run(defaultAgentType);
|
|
database.exec(`
|
|
DROP TABLE sessions;
|
|
ALTER TABLE sessions_new RENAME TO sessions;
|
|
`);
|
|
}
|
|
|
|
export function backfillLegacyServiceSessions(
|
|
database: Database,
|
|
resolveAgentTypeFromServiceId: ServiceShadowAgentTypeResolver,
|
|
): void {
|
|
if (!hasLegacyServiceSessionsTable(database)) {
|
|
return;
|
|
}
|
|
|
|
const rows = database
|
|
.prepare(
|
|
`SELECT group_folder, service_id, session_id
|
|
FROM service_sessions`,
|
|
)
|
|
.all() as Array<{
|
|
group_folder: string;
|
|
service_id: string;
|
|
session_id: string;
|
|
}>;
|
|
|
|
const upsert = database.prepare(
|
|
`INSERT OR IGNORE INTO sessions (group_folder, agent_type, session_id)
|
|
VALUES (?, ?, ?)`,
|
|
);
|
|
|
|
const tx = database.transaction(
|
|
(
|
|
sessionRows: Array<{
|
|
group_folder: string;
|
|
service_id: string;
|
|
session_id: string;
|
|
}>,
|
|
) => {
|
|
for (const row of sessionRows) {
|
|
const agentType = resolveAgentTypeFromServiceId(row.service_id);
|
|
if (!agentType) {
|
|
continue;
|
|
}
|
|
upsert.run(row.group_folder, agentType, row.session_id);
|
|
}
|
|
},
|
|
);
|
|
|
|
tx(rows);
|
|
}
|
|
|
|
export function dropLegacyServiceSessionsTable(database: Database): void {
|
|
if (!hasLegacyServiceSessionsTable(database)) {
|
|
return;
|
|
}
|
|
|
|
database.exec(`DROP TABLE service_sessions`);
|
|
}
|
|
|
|
export function getSessionFromDatabase(
|
|
database: Database,
|
|
groupFolder: string,
|
|
agentType: string,
|
|
): string | undefined {
|
|
const row = database
|
|
.prepare(
|
|
'SELECT session_id FROM sessions WHERE group_folder = ? AND agent_type = ?',
|
|
)
|
|
.get(groupFolder, agentType) as { session_id: string } | undefined;
|
|
return row?.session_id;
|
|
}
|
|
|
|
export function setSessionInDatabase(
|
|
database: Database,
|
|
groupFolder: string,
|
|
agentType: string,
|
|
sessionId: string,
|
|
): void {
|
|
database
|
|
.prepare(
|
|
'INSERT OR REPLACE INTO sessions (group_folder, agent_type, session_id) VALUES (?, ?, ?)',
|
|
)
|
|
.run(groupFolder, agentType, sessionId);
|
|
}
|
|
|
|
export function deleteSessionFromDatabase(
|
|
database: Database,
|
|
groupFolder: string,
|
|
agentType: string,
|
|
): void {
|
|
database
|
|
.prepare('DELETE FROM sessions WHERE group_folder = ? AND agent_type = ?')
|
|
.run(groupFolder, agentType);
|
|
}
|
|
|
|
export function deleteAllSessionsForGroupFromDatabase(
|
|
database: Database,
|
|
groupFolder: string,
|
|
): void {
|
|
database
|
|
.prepare(
|
|
`DELETE FROM sessions
|
|
WHERE group_folder IN (?, ?, ?)`,
|
|
)
|
|
.run(groupFolder, `${groupFolder}:reviewer`, `${groupFolder}:arbiter`);
|
|
}
|
|
|
|
export function getAllSessionsForAgentTypeFromDatabase(
|
|
database: Database,
|
|
agentType: string,
|
|
): Record<string, string> {
|
|
const rows = database
|
|
.prepare(
|
|
'SELECT group_folder, session_id FROM sessions WHERE agent_type = ?',
|
|
)
|
|
.all(agentType) as Array<{
|
|
group_folder: string;
|
|
session_id: string;
|
|
}>;
|
|
const result: Record<string, string> = {};
|
|
for (const row of rows) {
|
|
result[row.group_folder] = row.session_id;
|
|
}
|
|
return result;
|
|
}
|