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) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-25 23:46:33 -04:00
1 parent aa1a78d7d9
commit 122cf8db04
1 file changed
+59
+59
View File
@@ -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) {