|
|
|
@@ -1,6 +1,17 @@
|
|
|
|
|
import Database from 'better-sqlite3';
|
|
|
|
|
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
|
|
|
|
import { logger } from '../../utils/logger.js';
|
|
|
|
|
import {
|
|
|
|
|
TableColumnInfo,
|
|
|
|
|
IndexInfo,
|
|
|
|
|
TableNameRow,
|
|
|
|
|
SchemaVersion,
|
|
|
|
|
SdkSessionRecord,
|
|
|
|
|
ObservationRecord,
|
|
|
|
|
SessionSummaryRecord,
|
|
|
|
|
UserPromptRecord,
|
|
|
|
|
LatestPromptResult
|
|
|
|
|
} from '../../types/database.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Session data store for SDK sessions, observations, and summaries
|
|
|
|
@@ -47,7 +58,7 @@ export class SessionStore {
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
// Get applied migrations
|
|
|
|
|
const appliedVersions = this.db.prepare('SELECT version FROM schema_versions ORDER BY version').all() as Array<{version: number}>;
|
|
|
|
|
const appliedVersions = this.db.prepare('SELECT version FROM schema_versions ORDER BY version').all() as SchemaVersion[];
|
|
|
|
|
const maxApplied = appliedVersions.length > 0 ? Math.max(...appliedVersions.map(v => v.version)) : 0;
|
|
|
|
|
|
|
|
|
|
// Only run migration004 if no migrations have been applied
|
|
|
|
@@ -131,12 +142,12 @@ export class SessionStore {
|
|
|
|
|
private ensureWorkerPortColumn(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Check if migration already applied
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(5) as {version: number} | undefined;
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(5) as SchemaVersion | undefined;
|
|
|
|
|
if (applied) return;
|
|
|
|
|
|
|
|
|
|
// Check if column exists
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(sdk_sessions)');
|
|
|
|
|
const hasWorkerPort = (tableInfo as any[]).some((col: any) => col.name === 'worker_port');
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(sdk_sessions)') as TableColumnInfo[];
|
|
|
|
|
const hasWorkerPort = tableInfo.some(col => col.name === 'worker_port');
|
|
|
|
|
|
|
|
|
|
if (!hasWorkerPort) {
|
|
|
|
|
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
|
|
|
|
@@ -156,12 +167,12 @@ export class SessionStore {
|
|
|
|
|
private ensurePromptTrackingColumns(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Check if migration already applied
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(6) as {version: number} | undefined;
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(6) as SchemaVersion | undefined;
|
|
|
|
|
if (applied) return;
|
|
|
|
|
|
|
|
|
|
// Check sdk_sessions for prompt_counter
|
|
|
|
|
const sessionsInfo = this.db.pragma('table_info(sdk_sessions)');
|
|
|
|
|
const hasPromptCounter = (sessionsInfo as any[]).some((col: any) => col.name === 'prompt_counter');
|
|
|
|
|
const sessionsInfo = this.db.pragma('table_info(sdk_sessions)') as TableColumnInfo[];
|
|
|
|
|
const hasPromptCounter = sessionsInfo.some(col => col.name === 'prompt_counter');
|
|
|
|
|
|
|
|
|
|
if (!hasPromptCounter) {
|
|
|
|
|
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0');
|
|
|
|
@@ -169,8 +180,8 @@ export class SessionStore {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check observations for prompt_number
|
|
|
|
|
const observationsInfo = this.db.pragma('table_info(observations)');
|
|
|
|
|
const obsHasPromptNumber = (observationsInfo as any[]).some((col: any) => col.name === 'prompt_number');
|
|
|
|
|
const observationsInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
|
|
|
|
|
const obsHasPromptNumber = observationsInfo.some(col => col.name === 'prompt_number');
|
|
|
|
|
|
|
|
|
|
if (!obsHasPromptNumber) {
|
|
|
|
|
this.db.exec('ALTER TABLE observations ADD COLUMN prompt_number INTEGER');
|
|
|
|
@@ -178,8 +189,8 @@ export class SessionStore {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check session_summaries for prompt_number
|
|
|
|
|
const summariesInfo = this.db.pragma('table_info(session_summaries)');
|
|
|
|
|
const sumHasPromptNumber = (summariesInfo as any[]).some((col: any) => col.name === 'prompt_number');
|
|
|
|
|
const summariesInfo = this.db.pragma('table_info(session_summaries)') as TableColumnInfo[];
|
|
|
|
|
const sumHasPromptNumber = summariesInfo.some(col => col.name === 'prompt_number');
|
|
|
|
|
|
|
|
|
|
if (!sumHasPromptNumber) {
|
|
|
|
|
this.db.exec('ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER');
|
|
|
|
@@ -199,12 +210,12 @@ export class SessionStore {
|
|
|
|
|
private removeSessionSummariesUniqueConstraint(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Check if migration already applied
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(7) as {version: number} | undefined;
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(7) as SchemaVersion | undefined;
|
|
|
|
|
if (applied) return;
|
|
|
|
|
|
|
|
|
|
// Check if UNIQUE constraint exists
|
|
|
|
|
const summariesIndexes = this.db.pragma('index_list(session_summaries)');
|
|
|
|
|
const hasUniqueConstraint = (summariesIndexes as any[]).some((idx: any) => idx.unique === 1);
|
|
|
|
|
const summariesIndexes = this.db.pragma('index_list(session_summaries)') as IndexInfo[];
|
|
|
|
|
const hasUniqueConstraint = summariesIndexes.some(idx => idx.unique === 1);
|
|
|
|
|
|
|
|
|
|
if (!hasUniqueConstraint) {
|
|
|
|
|
// Already migrated (no constraint exists)
|
|
|
|
@@ -284,12 +295,12 @@ export class SessionStore {
|
|
|
|
|
private addObservationHierarchicalFields(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Check if migration already applied
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(8) as {version: number} | undefined;
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(8) as SchemaVersion | undefined;
|
|
|
|
|
if (applied) return;
|
|
|
|
|
|
|
|
|
|
// Check if new fields already exist
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(observations)');
|
|
|
|
|
const hasTitle = (tableInfo as any[]).some((col: any) => col.name === 'title');
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
|
|
|
|
|
const hasTitle = tableInfo.some(col => col.name === 'title');
|
|
|
|
|
|
|
|
|
|
if (hasTitle) {
|
|
|
|
|
// Already migrated
|
|
|
|
@@ -326,12 +337,12 @@ export class SessionStore {
|
|
|
|
|
private makeObservationsTextNullable(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Check if migration already applied
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(9) as {version: number} | undefined;
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(9) as SchemaVersion | undefined;
|
|
|
|
|
if (applied) return;
|
|
|
|
|
|
|
|
|
|
// Check if text column is already nullable
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(observations)');
|
|
|
|
|
const textColumn = (tableInfo as any[]).find((col: any) => col.name === 'text');
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
|
|
|
|
|
const textColumn = tableInfo.find(col => col.name === 'text');
|
|
|
|
|
|
|
|
|
|
if (!textColumn || textColumn.notnull === 0) {
|
|
|
|
|
// Already migrated or text column doesn't exist
|
|
|
|
@@ -413,12 +424,12 @@ export class SessionStore {
|
|
|
|
|
private createUserPromptsTable(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Check if migration already applied
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(10) as {version: number} | undefined;
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(10) as SchemaVersion | undefined;
|
|
|
|
|
if (applied) return;
|
|
|
|
|
|
|
|
|
|
// Check if table already exists
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(user_prompts)');
|
|
|
|
|
if ((tableInfo as any[]).length > 0) {
|
|
|
|
|
const tableInfo = this.db.pragma('table_info(user_prompts)') as TableColumnInfo[];
|
|
|
|
|
if (tableInfo.length > 0) {
|
|
|
|
|
// Already migrated
|
|
|
|
|
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(10, new Date().toISOString());
|
|
|
|
|
return;
|
|
|
|
@@ -502,12 +513,12 @@ export class SessionStore {
|
|
|
|
|
private ensureDiscoveryTokensColumn(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Check if migration already applied to avoid unnecessary re-runs
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(11) as {version: number} | undefined;
|
|
|
|
|
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(11) as SchemaVersion | undefined;
|
|
|
|
|
if (applied) return;
|
|
|
|
|
|
|
|
|
|
// Check if discovery_tokens column exists in observations table
|
|
|
|
|
const observationsInfo = this.db.pragma('table_info(observations)');
|
|
|
|
|
const obsHasDiscoveryTokens = (observationsInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
|
|
|
|
|
const observationsInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
|
|
|
|
|
const obsHasDiscoveryTokens = observationsInfo.some(col => col.name === 'discovery_tokens');
|
|
|
|
|
|
|
|
|
|
if (!obsHasDiscoveryTokens) {
|
|
|
|
|
this.db.exec('ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
|
|
|
|
@@ -515,8 +526,8 @@ export class SessionStore {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if discovery_tokens column exists in session_summaries table
|
|
|
|
|
const summariesInfo = this.db.pragma('table_info(session_summaries)');
|
|
|
|
|
const sumHasDiscoveryTokens = (summariesInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
|
|
|
|
|
const summariesInfo = this.db.pragma('table_info(session_summaries)') as TableColumnInfo[];
|
|
|
|
|
const sumHasDiscoveryTokens = summariesInfo.some(col => col.name === 'discovery_tokens');
|
|
|
|
|
|
|
|
|
|
if (!sumHasDiscoveryTokens) {
|
|
|
|
|
this.db.exec('ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
|
|
|
|
@@ -556,7 +567,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(project, limit) as any[];
|
|
|
|
|
return stmt.all(project, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -581,7 +592,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(project, limit) as any[];
|
|
|
|
|
return stmt.all(project, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -601,7 +612,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(project, limit) as any[];
|
|
|
|
|
return stmt.all(project, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -625,7 +636,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(limit) as any[];
|
|
|
|
|
return stmt.all(limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -655,7 +666,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(limit) as any[];
|
|
|
|
|
return stmt.all(limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -685,7 +696,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(limit) as any[];
|
|
|
|
|
return stmt.all(limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -728,7 +739,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.get(claudeSessionId) as any;
|
|
|
|
|
return stmt.get(claudeSessionId) as LatestPromptResult | undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -760,7 +771,7 @@ export class SessionStore {
|
|
|
|
|
ORDER BY started_at_epoch ASC
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(project, limit) as any[];
|
|
|
|
|
return stmt.all(project, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -779,20 +790,20 @@ export class SessionStore {
|
|
|
|
|
ORDER BY created_at_epoch ASC
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(sdkSessionId) as any[];
|
|
|
|
|
return stmt.all(sdkSessionId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a single observation by ID
|
|
|
|
|
*/
|
|
|
|
|
getObservationById(id: number): any | null {
|
|
|
|
|
getObservationById(id: number): ObservationRecord | null {
|
|
|
|
|
const stmt = this.db.prepare(`
|
|
|
|
|
SELECT *
|
|
|
|
|
FROM observations
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.get(id) as any || null;
|
|
|
|
|
return stmt.get(id) as ObservationRecord | undefined || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -801,7 +812,7 @@ export class SessionStore {
|
|
|
|
|
getObservationsByIds(
|
|
|
|
|
ids: number[],
|
|
|
|
|
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
|
|
|
|
): any[] {
|
|
|
|
|
): ObservationRecord[] {
|
|
|
|
|
if (ids.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const { orderBy = 'date_desc', limit } = options;
|
|
|
|
@@ -819,7 +830,7 @@ export class SessionStore {
|
|
|
|
|
${limitClause}
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(...ids) as any[];
|
|
|
|
|
return stmt.all(...ids) as ObservationRecord[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -847,7 +858,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.get(sdkSessionId) as any || null;
|
|
|
|
|
return stmt.get(sdkSessionId) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -920,7 +931,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.get(id) as any || null;
|
|
|
|
|
return stmt.get(id) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -939,7 +950,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.get(claudeSessionId) as any || null;
|
|
|
|
|
return stmt.get(claudeSessionId) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -953,7 +964,7 @@ export class SessionStore {
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.get(claudeSessionId) as any || null;
|
|
|
|
|
return stmt.get(claudeSessionId) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -1343,7 +1354,7 @@ export class SessionStore {
|
|
|
|
|
getSessionSummariesByIds(
|
|
|
|
|
ids: number[],
|
|
|
|
|
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
|
|
|
|
): any[] {
|
|
|
|
|
): SessionSummaryRecord[] {
|
|
|
|
|
if (ids.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const { orderBy = 'date_desc', limit } = options;
|
|
|
|
@@ -1358,7 +1369,7 @@ export class SessionStore {
|
|
|
|
|
${limitClause}
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(...ids) as any[];
|
|
|
|
|
return stmt.all(...ids) as SessionSummaryRecord[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -1368,7 +1379,7 @@ export class SessionStore {
|
|
|
|
|
getUserPromptsByIds(
|
|
|
|
|
ids: number[],
|
|
|
|
|
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
|
|
|
|
): any[] {
|
|
|
|
|
): UserPromptRecord[] {
|
|
|
|
|
if (ids.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const { orderBy = 'date_desc', limit } = options;
|
|
|
|
@@ -1388,7 +1399,7 @@ export class SessionStore {
|
|
|
|
|
${limitClause}
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return stmt.all(...ids) as any[];
|
|
|
|
|
return stmt.all(...ids) as UserPromptRecord[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -1451,8 +1462,8 @@ export class SessionStore {
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const beforeRecords = this.db.prepare(beforeQuery).all(anchorObservationId, ...projectParams, depthBefore + 1) as any[];
|
|
|
|
|
const afterRecords = this.db.prepare(afterQuery).all(anchorObservationId, ...projectParams, depthAfter + 1) as any[];
|
|
|
|
|
const beforeRecords = this.db.prepare(beforeQuery).all(anchorObservationId, ...projectParams, depthBefore + 1) as Array<{id: number; created_at_epoch: number}>;
|
|
|
|
|
const afterRecords = this.db.prepare(afterQuery).all(anchorObservationId, ...projectParams, depthAfter + 1) as Array<{id: number; created_at_epoch: number}>;
|
|
|
|
|
|
|
|
|
|
// Get the earliest and latest timestamps from boundary observations
|
|
|
|
|
if (beforeRecords.length === 0 && afterRecords.length === 0) {
|
|
|
|
@@ -1484,8 +1495,8 @@ export class SessionStore {
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const beforeRecords = this.db.prepare(beforeQuery).all(anchorEpoch, ...projectParams, depthBefore) as any[];
|
|
|
|
|
const afterRecords = this.db.prepare(afterQuery).all(anchorEpoch, ...projectParams, depthAfter + 1) as any[];
|
|
|
|
|
const beforeRecords = this.db.prepare(beforeQuery).all(anchorEpoch, ...projectParams, depthBefore) as Array<{created_at_epoch: number}>;
|
|
|
|
|
const afterRecords = this.db.prepare(afterQuery).all(anchorEpoch, ...projectParams, depthAfter + 1) as Array<{created_at_epoch: number}>;
|
|
|
|
|
|
|
|
|
|
if (beforeRecords.length === 0 && afterRecords.length === 0) {
|
|
|
|
|
return { observations: [], sessions: [], prompts: [] };
|
|
|
|
@@ -1523,9 +1534,9 @@ export class SessionStore {
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const observations = this.db.prepare(obsQuery).all(startEpoch, endEpoch, ...projectParams) as any[];
|
|
|
|
|
const sessions = this.db.prepare(sessQuery).all(startEpoch, endEpoch, ...projectParams) as any[];
|
|
|
|
|
const prompts = this.db.prepare(promptQuery).all(startEpoch, endEpoch, ...projectParams) as any[];
|
|
|
|
|
const observations = this.db.prepare(obsQuery).all(startEpoch, endEpoch, ...projectParams) as ObservationRecord[];
|
|
|
|
|
const sessions = this.db.prepare(sessQuery).all(startEpoch, endEpoch, ...projectParams) as SessionSummaryRecord[];
|
|
|
|
|
const prompts = this.db.prepare(promptQuery).all(startEpoch, endEpoch, ...projectParams) as UserPromptRecord[];
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
observations,
|
|
|
|
|