From 122cf8db04de546fb46082869ab2eb9662d8176d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 25 Mar 2026 23:46:33 -0400 Subject: [PATCH] feat(01-02): session data migration and backward compatibility verification Adds migration 15 - eager copy session_json to task_sessions table. Handles corrupt rows gracefully, maintains idempotency. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/database.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/main/database.ts b/src/main/database.ts index 1e2a7517..274f63c8 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -2,6 +2,7 @@ import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readF import { appendFile, copyFile, cp, rm } from "node:fs/promises"; import { extname, join, resolve, sep } from "node:path"; import { randomUUID } from "node:crypto"; +import log from "electron-log/main.js"; import { DatabaseSync, backup as backupDatabase, type SQLInputValue } from "node:sqlite"; import { getConnectorDefinition, buildDefaultConnectorRows, listInvalidEnvBindingKeys, parseEnvBindingText } from "./connectors"; import { SecretVault } from "./vault"; @@ -3328,6 +3329,64 @@ export class AppDatabase { CREATE INDEX IF NOT EXISTS idx_wakeup_requests_agent_requested ON wakeup_requests(agent_id, requested_at) `, }, + { + version: 15, + name: "migrate_session_data_to_task_sessions", + fn: () => { + // D-01: Eager copy of session_json to task_sessions table + // Only process tasks that have session data but no active_session_id yet (idempotent) + const tasksWithSessions = this.readAll<{ + id: string; + companyId: string; + assigneeAgentId: string | null; + sessionJson: string | null; + sessionDisplayId: string | null; + }>( + `SELECT id, company_id as companyId, assignee_agent_id as assigneeAgentId, + session_json as sessionJson, session_display_id as sessionDisplayId + FROM tasks WHERE session_json IS NOT NULL AND active_session_id IS NULL` + ); + + let migrated = 0; + let skipped = 0; + + for (const task of tasksWithSessions) { + try { + // Validate JSON is parseable (D-02: skip corrupt rows) + JSON.parse(task.sessionJson!); + + // Look up agent's connector_id for adapter_type + const agent = task.assigneeAgentId + ? this.db.prepare("SELECT connector_id FROM agents WHERE id = ?").get(task.assigneeAgentId) as { connector_id: string } | undefined + : undefined; + const adapterType = agent?.connector_id ?? "unknown"; + const sessionId = randomUUID(); + const ts = nowIso(); + + this.db.prepare( + `INSERT INTO task_sessions (id, company_id, task_id, agent_id, adapter_type, + session_params_json, session_display_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + sessionId, task.companyId, task.id, + task.assigneeAgentId ?? "unassigned", adapterType, + task.sessionJson, task.sessionDisplayId, ts, ts + ); + + this.db.prepare("UPDATE tasks SET active_session_id = ? WHERE id = ?") + .run(sessionId, task.id); + + migrated++; + } catch (err) { + // D-02: Skip bad rows, log warning, don't block app startup + log.warn(`Session migration skipped task ${task.id}: ${err instanceof Error ? err.message : String(err)}`); + skipped++; + } + } + + log.info(`Session migration complete: ${migrated} migrated, ${skipped} skipped`); + }, + }, ]; for (const migration of migrations) {