fix: make migration 17 fully idempotent for databases in intermediate states (#481)

* fix: make migration 17 idempotent and standardize column names

Migration 17 renamed columns from sdk_session_id to memory_session_id,
but the migration wasn't fully idempotent - it could fail on databases
in intermediate states. This caused errors like:
- "no such column: sdk_session_id" (when columns already renamed)
- "table observations has no column named memory_session_id" (when not renamed)

Changes:
- Rewrite renameSessionIdColumns() to check each table individually
- Use safeRenameColumn() helper that handles all edge cases gracefully
- Deprecate migration 19 (repair migration) since 17 is now idempotent
- Update maintenance scripts to use memory_session_id column name
- Update test files to use new column names

Fixes column mismatch bug in v8.2.6+

* Merge origin/main into column-mismatch

---------

Co-authored-by: Alex Newman <thedotmack@gmail.com>
This commit is contained in:
Lindsey Catlett
2025-12-29 23:30:04 -05:00
committed by GitHub
parent 61a23a14a9
commit d9e966d8f4
11 changed files with 198 additions and 240 deletions
+11 -11
View File
@@ -12,7 +12,7 @@ import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
interface ObservationRecord {
id: number;
sdk_session_id: string;
memory_session_id: string;
project: string;
text: string | null;
type: string;
@@ -31,8 +31,8 @@ interface ObservationRecord {
interface SdkSessionRecord {
id: number;
claude_session_id: string;
sdk_session_id: string;
content_session_id: string;
memory_session_id: string;
project: string;
user_prompt: string;
started_at: string;
@@ -44,7 +44,7 @@ interface SdkSessionRecord {
interface SessionSummaryRecord {
id: number;
sdk_session_id: string;
memory_session_id: string;
project: string;
request: string | null;
investigated: string | null;
@@ -62,7 +62,7 @@ interface SessionSummaryRecord {
interface UserPromptRecord {
id: number;
claude_session_id: string;
content_session_id: string;
prompt_number: number;
prompt_text: string;
created_at: string;
@@ -117,23 +117,23 @@ async function exportMemories(query: string, outputFile: string, project?: strin
console.log(`✅ Found ${summaries.length} session summaries`);
console.log(`✅ Found ${prompts.length} user prompts`);
// Get unique SDK session IDs from observations and summaries
const sdkSessionIds = new Set<string>();
// Get unique memory session IDs from observations and summaries
const memorySessionIds = new Set<string>();
observations.forEach((o) => {
if (o.sdk_session_id) sdkSessionIds.add(o.sdk_session_id);
if (o.memory_session_id) memorySessionIds.add(o.memory_session_id);
});
summaries.forEach((s) => {
if (s.sdk_session_id) sdkSessionIds.add(s.sdk_session_id);
if (s.memory_session_id) memorySessionIds.add(s.memory_session_id);
});
// Get SDK sessions metadata via API
console.log('📡 Fetching SDK sessions metadata...');
let sessions: SdkSessionRecord[] = [];
if (sdkSessionIds.size > 0) {
if (memorySessionIds.size > 0) {
const sessionsResponse = await fetch(`${baseUrl}/api/sdk-sessions/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sdkSessionIds: Array.from(sdkSessionIds) })
body: JSON.stringify({ sdkSessionIds: Array.from(memorySessionIds) })
});
if (sessionsResponse.ok) {
sessions = await sessionsResponse.json();
+3 -3
View File
@@ -18,7 +18,7 @@ interface CorruptedObservation {
obs_created: number;
session_started: number;
session_completed: number | null;
sdk_session_id: string;
memory_session_id: string;
}
function formatTimestamp(epoch: number): string {
@@ -54,9 +54,9 @@ function main() {
o.created_at_epoch as obs_created,
s.started_at_epoch as session_started,
s.completed_at_epoch as session_completed,
s.sdk_session_id
s.memory_session_id
FROM observations o
JOIN sdk_sessions s ON o.sdk_session_id = s.sdk_session_id
JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
WHERE o.created_at_epoch < s.started_at_epoch -- Observation older than session
OR (s.completed_at_epoch IS NOT NULL
AND o.created_at_epoch > (s.completed_at_epoch + 3600000)) -- More than 1hr after session
+6 -6
View File
@@ -20,7 +20,7 @@ const BAD_WINDOW_END = 1766626260000; // Dec 24 20:31 PST
interface AffectedObservation {
id: number;
sdk_session_id: string;
memory_session_id: string;
created_at_epoch: number;
title: string;
}
@@ -35,7 +35,7 @@ interface ProcessedMessage {
interface SessionMapping {
session_db_id: number;
sdk_session_id: string;
memory_session_id: string;
}
interface TimestampFix {
@@ -75,7 +75,7 @@ function main() {
// Step 1: Find affected observations
console.log('Step 1: Finding observations created during bad window...');
const affectedObs = db.query<AffectedObservation, []>(`
SELECT id, sdk_session_id, created_at_epoch, title
SELECT id, memory_session_id, created_at_epoch, title
FROM observations
WHERE created_at_epoch >= ${BAD_WINDOW_START}
AND created_at_epoch <= ${BAD_WINDOW_END}
@@ -111,7 +111,7 @@ function main() {
obs_title: string;
obs_created: number;
session_started: number;
sdk_session_id: string;
memory_session_id: string;
}
const obsWithSessions = db.query<ObsWithSession, []>(`
@@ -120,9 +120,9 @@ function main() {
o.title as obs_title,
o.created_at_epoch as obs_created,
s.started_at_epoch as session_started,
s.sdk_session_id
s.memory_session_id
FROM observations o
JOIN sdk_sessions s ON o.sdk_session_id = s.sdk_session_id
JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
WHERE o.created_at_epoch >= ${BAD_WINDOW_START}
AND o.created_at_epoch <= ${BAD_WINDOW_END}
AND s.started_at_epoch < ${BAD_WINDOW_START}
+2 -2
View File
@@ -36,7 +36,7 @@ function main() {
const dec24End = 1735113600000; // Dec 25 00:00 PST
const dec24Obs = db.query(`
SELECT id, sdk_session_id, created_at_epoch, title
SELECT id, memory_session_id, created_at_epoch, title
FROM observations
WHERE created_at_epoch >= ${dec24Start}
AND created_at_epoch < ${dec24End}
@@ -59,7 +59,7 @@ function main() {
const dec21Start = 1734768000000; // Dec 21 00:00 PST
const oldObs = db.query(`
SELECT id, sdk_session_id, created_at_epoch, title
SELECT id, memory_session_id, created_at_epoch, title
FROM observations
WHERE created_at_epoch >= ${dec17Start}
AND created_at_epoch < ${dec21Start}
+1 -1
View File
@@ -59,7 +59,7 @@ function main() {
pm.tool_name,
pm.created_at_epoch as msg_created,
pm.status,
s.sdk_session_id,
s.memory_session_id,
s.started_at_epoch as session_started,
s.project
FROM pending_messages pm
+7 -7
View File
@@ -22,7 +22,7 @@ const ORIGINAL_WINDOW_END = 1766613600000; // Dec 23 23:59 PST
interface Observation {
id: number;
sdk_session_id: string;
memory_session_id: string;
created_at_epoch: number;
created_at: string;
title: string;
@@ -49,7 +49,7 @@ function main() {
// Check 1: Observations still in bad window
console.log('Check 1: Looking for observations still in bad window (Dec 24 19:45-20:31)...');
const badWindowObs = db.query<Observation, []>(`
SELECT id, sdk_session_id, created_at_epoch, created_at, title
SELECT id, memory_session_id, created_at_epoch, created_at, title
FROM observations
WHERE created_at_epoch >= ${BAD_WINDOW_START}
AND created_at_epoch <= ${BAD_WINDOW_END}
@@ -63,7 +63,7 @@ function main() {
for (const obs of badWindowObs) {
console.log(` Observation #${obs.id}: ${obs.title || '(no title)'}`);
console.log(` Timestamp: ${formatTimestamp(obs.created_at_epoch)}`);
console.log(` Session: ${obs.sdk_session_id}\n`);
console.log(` Session: ${obs.memory_session_id}\n`);
}
}
@@ -81,19 +81,19 @@ function main() {
// Check 3: Session distribution
console.log('Check 3: Session distribution of corrected observations...');
const sessionDist = db.query<{ sdk_session_id: string; count: number }, []>(`
SELECT sdk_session_id, COUNT(*) as count
const sessionDist = db.query<{ memory_session_id: string; count: number }, []>(`
SELECT memory_session_id, COUNT(*) as count
FROM observations
WHERE created_at_epoch >= ${ORIGINAL_WINDOW_START}
AND created_at_epoch <= ${ORIGINAL_WINDOW_END}
GROUP BY sdk_session_id
GROUP BY memory_session_id
ORDER BY count DESC
`).all();
if (sessionDist.length > 0) {
console.log(`Observations distributed across ${sessionDist.length} sessions:\n`);
for (const dist of sessionDist.slice(0, 10)) {
console.log(` ${dist.sdk_session_id}: ${dist.count} observations`);
console.log(` ${dist.memory_session_id}: ${dist.count} observations`);
}
if (sessionDist.length > 10) {
console.log(` ... and ${sessionDist.length - 10} more sessions`);