MAESTRO: fix(db): prevent FK constraint failures on worker restart

Cherry-picked source changes from PR #889 by @Et9797. Fixes #846.

Key changes:
- Add ensureMemorySessionIdRegistered() guard in SessionStore.ts
- Add ON UPDATE CASCADE migration (schema v21) for observations and session_summaries FK constraints
- Change message queue from claim-and-delete to claim-confirm pattern (PendingMessageStore.ts)
- Add spawn deduplication and unrecoverable error detection in SessionRoutes.ts and worker-service.ts
- Add forceInit flag to SDKAgent for stale session recovery

Build artifacts skipped (pre-existing dompurify dep issue). Path fixes (HealthMonitor.ts, worker-utils.ts)
already merged via PR #634.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-02-06 03:16:17 -05:00
parent 7ed1e576b2
commit da1d2cd36a
20 changed files with 1136 additions and 150 deletions
+18 -8
View File
@@ -95,7 +95,9 @@ describe('GeminiAgent', () => {
storeObservation: mockStoreObservation,
storeObservations: mockStoreObservations, // Required by ResponseProcessor.ts
storeSummary: mockStoreSummary,
markSessionCompleted: mockMarkSessionCompleted
markSessionCompleted: mockMarkSessionCompleted,
getSessionById: mock(() => ({ memory_session_id: 'mem-session-123' })), // Required by ResponseProcessor.ts for FK fix
ensureMemorySessionIdRegistered: mock(() => {}) // Required by ResponseProcessor.ts for FK constraint fix (Issue #846)
};
const mockChromaSync = {
@@ -110,6 +112,7 @@ describe('GeminiAgent', () => {
const mockPendingMessageStore = {
markProcessed: mockMarkProcessed,
confirmProcessed: mock(() => {}), // CLAIM-CONFIRM pattern: confirm after successful storage
cleanupProcessed: mockCleanupProcessed,
resetStuckMessages: mockResetStuckMessages
};
@@ -148,7 +151,8 @@ describe('GeminiAgent', () => {
generatorPromise: null,
earliestPendingTimestamp: null,
currentProvider: null,
startTime: Date.now()
startTime: Date.now(),
processingMessageIds: [] // CLAIM-CONFIRM pattern: track message IDs being processed
} as any;
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({
@@ -184,7 +188,8 @@ describe('GeminiAgent', () => {
generatorPromise: null,
earliestPendingTimestamp: null,
currentProvider: null,
startTime: Date.now()
startTime: Date.now(),
processingMessageIds: [] // CLAIM-CONFIRM pattern: track message IDs being processed
} as any;
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({
@@ -216,7 +221,8 @@ describe('GeminiAgent', () => {
generatorPromise: null,
earliestPendingTimestamp: null,
currentProvider: null,
startTime: Date.now()
startTime: Date.now(),
processingMessageIds: [] // CLAIM-CONFIRM pattern: track message IDs being processed
} as any;
const observationXml = `
@@ -261,7 +267,8 @@ describe('GeminiAgent', () => {
generatorPromise: null,
earliestPendingTimestamp: null,
currentProvider: null,
startTime: Date.now()
startTime: Date.now(),
processingMessageIds: [] // CLAIM-CONFIRM pattern: track message IDs being processed
} as any;
global.fetch = mock(() => Promise.resolve(new Response('Resource has been exhausted (e.g. check quota).', { status: 429 })));
@@ -294,7 +301,8 @@ describe('GeminiAgent', () => {
generatorPromise: null,
earliestPendingTimestamp: null,
currentProvider: null,
startTime: Date.now()
startTime: Date.now(),
processingMessageIds: [] // CLAIM-CONFIRM pattern: track message IDs being processed
} as any;
global.fetch = mock(() => Promise.resolve(new Response('Invalid argument', { status: 400 })));
@@ -333,7 +341,8 @@ describe('GeminiAgent', () => {
generatorPromise: null,
earliestPendingTimestamp: null,
currentProvider: null,
startTime: Date.now()
startTime: Date.now(),
processingMessageIds: [] // CLAIM-CONFIRM pattern: track message IDs being processed
} as any;
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({
@@ -385,7 +394,8 @@ describe('GeminiAgent', () => {
generatorPromise: null,
earliestPendingTimestamp: null,
currentProvider: null,
startTime: Date.now()
startTime: Date.now(),
processingMessageIds: [] // CLAIM-CONFIRM pattern: track message IDs being processed
} as any;
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({