From c3e5f3a79e039a73f24614a65be112c7786c545b Mon Sep 17 00:00:00 2001 From: Alessandro Costa Date: Sat, 4 Apr 2026 21:25:18 -0300 Subject: [PATCH 1/2] fix: wire generated_by_model into observation write path The generated_by_model column was added to the observations table in the Phase 0 governance schema migration but never wired into the INSERT statements. All 3,878+ observations in production have this field NULL. This fix threads the model ID from each agent (SDKAgent, GeminiAgent, OpenRouterAgent) through processAgentResponse() into storeObservation(), storeObservations(), and storeObservationsAndMarkComplete(). Unblocks Thompson Sampling RFC (#1571) which needs {obs_type}:{model} as the bandit arm key. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/services/sqlite/SessionStore.ts | 33 ++++++++++++------- src/services/worker/GeminiAgent.ts | 10 ++++-- src/services/worker/OpenRouterAgent.ts | 9 +++-- src/services/worker/SDKAgent.ts | 3 +- .../worker/agents/ResponseProcessor.ts | 6 ++-- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/services/sqlite/SessionStore.ts b/src/services/sqlite/SessionStore.ts index aaaf8754..3fee848d 100644 --- a/src/services/sqlite/SessionStore.ts +++ b/src/services/sqlite/SessionStore.ts @@ -1517,7 +1517,8 @@ export class SessionStore { }, promptNumber?: number, discoveryTokens: number = 0, - overrideTimestampEpoch?: number + overrideTimestampEpoch?: number, + generatedByModel?: string ): { id: number; createdAtEpoch: number } { // Use override timestamp if provided (for processing backlog messages with original timestamps) const timestampEpoch = overrideTimestampEpoch ?? Date.now(); @@ -1533,8 +1534,9 @@ export class SessionStore { const stmt = this.db.prepare(` INSERT INTO observations (memory_session_id, project, type, title, subtitle, facts, narrative, concepts, - files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch, + generated_by_model) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( @@ -1552,7 +1554,8 @@ export class SessionStore { discoveryTokens, contentHash, timestampIso, - timestampEpoch + timestampEpoch, + generatedByModel || null ); return { @@ -1651,7 +1654,8 @@ export class SessionStore { } | null, promptNumber?: number, discoveryTokens: number = 0, - overrideTimestampEpoch?: number + overrideTimestampEpoch?: number, + generatedByModel?: string ): { observationIds: number[]; summaryId: number | null; createdAtEpoch: number } { // Use override timestamp if provided const timestampEpoch = overrideTimestampEpoch ?? Date.now(); @@ -1665,8 +1669,9 @@ export class SessionStore { const obsStmt = this.db.prepare(` INSERT INTO observations (memory_session_id, project, type, title, subtitle, facts, narrative, concepts, - files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch, + generated_by_model) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const observation of observations) { @@ -1693,7 +1698,8 @@ export class SessionStore { discoveryTokens, contentHash, timestampIso, - timestampEpoch + timestampEpoch, + generatedByModel || null ); observationIds.push(Number(result.lastInsertRowid)); } @@ -1780,7 +1786,8 @@ export class SessionStore { _pendingStore: PendingMessageStore, promptNumber?: number, discoveryTokens: number = 0, - overrideTimestampEpoch?: number + overrideTimestampEpoch?: number, + generatedByModel?: string ): { observationIds: number[]; summaryId?: number; createdAtEpoch: number } { // Use override timestamp if provided const timestampEpoch = overrideTimestampEpoch ?? Date.now(); @@ -1794,8 +1801,9 @@ export class SessionStore { const obsStmt = this.db.prepare(` INSERT INTO observations (memory_session_id, project, type, title, subtitle, facts, narrative, concepts, - files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch, + generated_by_model) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const observation of observations) { @@ -1822,7 +1830,8 @@ export class SessionStore { discoveryTokens, contentHash, timestampIso, - timestampEpoch + timestampEpoch, + generatedByModel || null ); observationIds.push(Number(result.lastInsertRowid)); } diff --git a/src/services/worker/GeminiAgent.ts b/src/services/worker/GeminiAgent.ts index 8bb69dc4..0f9a1e4e 100644 --- a/src/services/worker/GeminiAgent.ts +++ b/src/services/worker/GeminiAgent.ts @@ -175,7 +175,9 @@ export class GeminiAgent { worker, tokensUsed, null, - 'Gemini' + 'Gemini', + undefined, + model ); } else { logger.error('SDK', 'Empty Gemini init response - session may lack context', { @@ -248,7 +250,8 @@ export class GeminiAgent { tokensUsed, originalTimestamp, 'Gemini', - lastCwd + lastCwd, + model ); } else { logger.warn('SDK', 'Empty Gemini observation response, skipping processing to preserve message', { @@ -298,7 +301,8 @@ export class GeminiAgent { tokensUsed, originalTimestamp, 'Gemini', - lastCwd + lastCwd, + model ); } else { logger.warn('SDK', 'Empty Gemini summary response, skipping processing to preserve message', { diff --git a/src/services/worker/OpenRouterAgent.ts b/src/services/worker/OpenRouterAgent.ts index e1c77c96..f034987f 100644 --- a/src/services/worker/OpenRouterAgent.ts +++ b/src/services/worker/OpenRouterAgent.ts @@ -131,7 +131,8 @@ export class OpenRouterAgent { tokensUsed, null, 'OpenRouter', - undefined // No lastCwd yet - before message processing + undefined, // No lastCwd yet - before message processing + model ); } else { logger.error('SDK', 'Empty OpenRouter init response - session may lack context', { @@ -202,7 +203,8 @@ export class OpenRouterAgent { tokensUsed, originalTimestamp, 'OpenRouter', - lastCwd + lastCwd, + model ); } else if (message.type === 'summarize') { @@ -244,7 +246,8 @@ export class OpenRouterAgent { tokensUsed, originalTimestamp, 'OpenRouter', - lastCwd + lastCwd, + model ); } } diff --git a/src/services/worker/SDKAgent.ts b/src/services/worker/SDKAgent.ts index 5a9b5866..16efc691 100644 --- a/src/services/worker/SDKAgent.ts +++ b/src/services/worker/SDKAgent.ts @@ -270,7 +270,8 @@ export class SDKAgent { discoveryTokens, originalTimestamp, 'SDK', - cwdTracker.lastCwd + cwdTracker.lastCwd, + modelId ); } diff --git a/src/services/worker/agents/ResponseProcessor.ts b/src/services/worker/agents/ResponseProcessor.ts index 8573ea8f..da2440c1 100644 --- a/src/services/worker/agents/ResponseProcessor.ts +++ b/src/services/worker/agents/ResponseProcessor.ts @@ -54,7 +54,8 @@ export async function processAgentResponse( discoveryTokens: number, originalTimestamp: number | null, agentName: string, - projectRoot?: string + projectRoot?: string, + modelId?: string ): Promise { // Track generator activity for stale detection (Issue #1099) session.lastGeneratorActivity = Date.now(); @@ -102,7 +103,8 @@ export async function processAgentResponse( summaryForStore, session.lastPromptNumber, discoveryTokens, - originalTimestamp ?? undefined + originalTimestamp ?? undefined, + modelId ); // Log storage result with IDs for end-to-end traceability From 5e696888d630f16ec79f44d4c8976bbc5f6e71da Mon Sep 17 00:00:00 2001 From: Alessandro Costa Date: Mon, 6 Apr 2026 07:20:48 -0300 Subject: [PATCH 2/2] fix: add migration for generated_by_model and relevance_count columns The compiled binary (v10.6.3) creates these columns at runtime via MigrationRunner, but no corresponding migration exists in the TypeScript source. Anyone building from source gets observations without these columns, breaking the feedback pipeline and model tracking. This migration conditionally adds both columns using PRAGMA table_info checks, making it safe for databases that already have them. Refs: #1626 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/services/sqlite/migrations.ts | 34 ++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/services/sqlite/migrations.ts b/src/services/sqlite/migrations.ts index 5be21f6a..76d51a9b 100644 --- a/src/services/sqlite/migrations.ts +++ b/src/services/sqlite/migrations.ts @@ -541,6 +541,37 @@ export const migration008: Migration = { } }; +/** + * Migration 009: Add missing columns to observations table + * + * The generated_by_model column tracks which model generated each observation + * (required for model selection optimization via Thompson Sampling). + * The relevance_count column tracks how many times an observation was reused + * (incremented by the feedback recording pipeline). + * + * Both columns may already exist in databases created by the compiled binary + * (v10.6.3) but are missing from the migration source. This migration + * conditionally adds them. + */ +export const migration009: Migration = { + version: 26, + up: (db: Database) => { + const columns = db.prepare('PRAGMA table_info(observations)').all() as any[]; + const hasGeneratedByModel = columns.some((c: any) => c.name === 'generated_by_model'); + const hasRelevanceCount = columns.some((c: any) => c.name === 'relevance_count'); + + if (!hasGeneratedByModel) { + db.run('ALTER TABLE observations ADD COLUMN generated_by_model TEXT'); + } + if (!hasRelevanceCount) { + db.run('ALTER TABLE observations ADD COLUMN relevance_count INTEGER DEFAULT 0'); + } + }, + down: (_db: Database) => { + // SQLite does not support DROP COLUMN in older versions; no-op + } +}; + /** * All migrations in order */ @@ -552,5 +583,6 @@ export const migrations: Migration[] = [ migration005, migration006, migration007, - migration008 + migration008, + migration009 ]; \ No newline at end of file