refactor(session): drop dead task-scoped session columns (v41)
Sessions have been agent-scoped since v34, but the schema kept four
dead task-scoped columns that every session code path had to pretend
were meaningful:
- tasks.active_session_id (mirrored agents.session_id)
- tasks.session_json (superseded by task_sessions.session_params_json)
- tasks.session_display_id (superseded by task_sessions.session_display_id)
- task_sessions.task_id (always empty-string on new rows)
v41 drops all four via SQLite table rebuild — the only safe path when
multiple columns need to go in one step and indexes / FK directions
need to be preserved. `PRAGMA foreign_keys = OFF` spans the swap
window so cascade-delete does not fire mid-rebuild. The migration is
idempotent and also backfills the optional task columns
(task_type / requires_user_review / retry_count / last_failure_at /
started_at / completed_at / cancelled_at / checkout_run_id /
execution_locked_at) before rebuilding so it works whether or not
`ensureOptionalColumns` has run yet on this install.
Along with the schema change, every code path that read or wrote
those columns has been rewired to the agent-scoped equivalents:
- task-sessions-crud.ts: deleted updateTaskSession,
getTaskSession, getActiveTaskSession, updateTasksActiveSessionForAgent;
added updateSessionParams(sessionId, ...) for the worker's
finalizer. ensureAgentSession and createNewSession no longer touch
task_id.
- database-base.ts: facade drops the now-unused pass-throughs.
- snapshot.ts / task-queries.ts / tasks-status.ts / entity-crud.ts:
SELECT projections no longer pull session_json / session_display_id
/ active_session_id from tasks.
- tasks-crud.ts (TaskRow) and shared/types/records-core.ts
(TaskRecord): sessionJson / sessionDisplayId fields removed.
- TaskSessionRecord: taskId field removed.
- turn-context.ts::resolveSession: no more
updateTasksActiveSessionForAgent mirror; sessionParams read directly
from the current session via getSessionById.
- run-finished-helpers.ts::finalizeRun: stamps sessionParams via
updateSessionParams on the agent's current session, resolved through
agents.session_id.
- session-management-service.ts: rotateSession stops calling
updateTasksActiveSessionForAgent.
Test suite updated to use the new session-id API. Existing tests used
`getActiveTaskSession(taskId)` and `createNewSession({ taskId, ...})`
as probes; they now use `getSessionById(sessionId)` and the taskId-less
createNewSession input.
Tests: 603/603 green. Typecheck clean. Migration tested on fresh DB
(runs v1→v41 cleanly) and idempotent re-apply (detects absent dead
columns, skips).
This commit is contained in:
1 parent
6abc1a823f
commit
2ace9183b0
17 files changed
+270
-243
No files matched your search
@@ -0,0 +1,173 @@
|
||||
import log from "electron-log/main.js";
|
||||
import type { Migration, MigrationDeps } from "./types";
|
||||
import { columnExists, tableExists } from "./types";
|
||||
|
||||
/**
|
||||
* Drop dead columns left behind by the agent-scoped session refactor.
|
||||
*
|
||||
* Pre-v34, a session belonged to a task; the schema reflected that
|
||||
* with `tasks.active_session_id`, `tasks.session_json`,
|
||||
* `tasks.session_display_id`, and a `task_sessions.task_id` FK back to
|
||||
* tasks. Since v34, sessions are agent-scoped — `agents.session_id`
|
||||
* is the canonical pointer, and every runtime read goes through that
|
||||
* or through the session's primary key. The four columns have been
|
||||
* dead storage ever since, and every touch of session code had to
|
||||
* pretend they were still meaningful.
|
||||
*
|
||||
* This migration rebuilds both tables without those columns. SQLite
|
||||
* has no `ALTER TABLE DROP COLUMN` that drops multiple columns
|
||||
* atomically and preserves every constraint; the "copy to new table,
|
||||
* swap, reindex" pattern is the only safe route. `PRAGMA foreign_keys`
|
||||
* is disabled for the swap window and restored afterward so child
|
||||
* tables are not cascade-deleted mid-rebuild.
|
||||
*
|
||||
* The migration is idempotent: each table rebuild checks whether the
|
||||
* dead columns are still present and skips the work otherwise.
|
||||
*/
|
||||
export function createMigrationsV41V41(deps: MigrationDeps): Migration[] {
|
||||
const { db } = deps;
|
||||
return [
|
||||
{
|
||||
version: 41,
|
||||
name: "drop_dead_session_columns",
|
||||
fn: () => {
|
||||
if (!tableExists(db, "tasks") || !tableExists(db, "task_sessions")) {
|
||||
log.info("[migration v41] tasks or task_sessions table missing; skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const tasksHasDeadCols =
|
||||
columnExists(db, "tasks", "session_json") ||
|
||||
columnExists(db, "tasks", "session_display_id") ||
|
||||
columnExists(db, "tasks", "active_session_id");
|
||||
const sessionsHasDeadCol = columnExists(db, "task_sessions", "task_id");
|
||||
|
||||
if (!tasksHasDeadCols && !sessionsHasDeadCol) {
|
||||
log.info("[migration v41] dead columns already absent; skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// The `ensureOptionalColumns` helper in schema.ts normally
|
||||
// backfills `task_type`, `requires_user_review`, `retry_count`,
|
||||
// `last_failure_at` on existing installs — but that runs AFTER
|
||||
// migrations, not before. Rebuild the table here with the same
|
||||
// shape as the post-ensureOptionalColumns schema so the old data
|
||||
// is preserved and the new table stays consistent whether
|
||||
// initializeDatabase has reached ensureOptionalColumns yet or not.
|
||||
const ensureTaskCol = (column: string, typeSql: string) => {
|
||||
if (!columnExists(db, "tasks", column)) {
|
||||
db.prepare(`ALTER TABLE tasks ADD COLUMN ${column} ${typeSql}`).run();
|
||||
}
|
||||
};
|
||||
ensureTaskCol("task_type", "TEXT NOT NULL DEFAULT 'general'");
|
||||
ensureTaskCol("requires_user_review", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureTaskCol("retry_count", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureTaskCol("last_failure_at", "TEXT");
|
||||
ensureTaskCol("checkout_run_id", "TEXT");
|
||||
ensureTaskCol("execution_locked_at", "TEXT");
|
||||
ensureTaskCol("started_at", "TEXT");
|
||||
ensureTaskCol("completed_at", "TEXT");
|
||||
ensureTaskCol("cancelled_at", "TEXT");
|
||||
|
||||
db.exec("PRAGMA foreign_keys = OFF");
|
||||
try {
|
||||
if (tasksHasDeadCols) {
|
||||
db.exec(`
|
||||
CREATE TABLE tasks_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
company_id TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
goal_id TEXT,
|
||||
parent_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
assignee_agent_id TEXT,
|
||||
workspace_id TEXT,
|
||||
status TEXT NOT NULL,
|
||||
priority TEXT NOT NULL,
|
||||
active_run_id TEXT,
|
||||
cost_usd REAL NOT NULL DEFAULT 0,
|
||||
task_type TEXT NOT NULL DEFAULT 'general',
|
||||
requires_user_review INTEGER NOT NULL DEFAULT 0,
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_failure_at TEXT,
|
||||
checkout_run_id TEXT,
|
||||
execution_locked_at TEXT,
|
||||
started_at TEXT,
|
||||
completed_at TEXT,
|
||||
cancelled_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO tasks_new (
|
||||
id, company_id, project_id, goal_id, parent_id, title, description,
|
||||
assignee_agent_id, workspace_id, status, priority, active_run_id,
|
||||
cost_usd, task_type, requires_user_review, retry_count, last_failure_at,
|
||||
checkout_run_id, execution_locked_at, started_at, completed_at,
|
||||
cancelled_at, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
id, company_id, project_id, goal_id, parent_id, title, description,
|
||||
assignee_agent_id, workspace_id, status, priority, active_run_id,
|
||||
cost_usd, task_type, requires_user_review, retry_count, last_failure_at,
|
||||
checkout_run_id, execution_locked_at, started_at, completed_at,
|
||||
cancelled_at, created_at, updated_at
|
||||
FROM tasks;
|
||||
DROP TABLE tasks;
|
||||
ALTER TABLE tasks_new RENAME TO tasks;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_company ON tasks(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee_agent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_company_status ON tasks(company_id, status);
|
||||
`);
|
||||
log.info("[migration v41] rebuilt tasks without session_json/session_display_id/active_session_id");
|
||||
}
|
||||
|
||||
if (sessionsHasDeadCol) {
|
||||
db.exec(`
|
||||
CREATE TABLE task_sessions_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
company_id TEXT NOT NULL,
|
||||
agent_id TEXT NOT NULL,
|
||||
adapter_type TEXT NOT NULL,
|
||||
session_params_json TEXT,
|
||||
session_display_id TEXT,
|
||||
last_run_id TEXT,
|
||||
last_error TEXT,
|
||||
run_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
total_output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
rotated_at TEXT,
|
||||
rotation_reason TEXT,
|
||||
handoff_markdown TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO task_sessions_new (
|
||||
id, company_id, agent_id, adapter_type, session_params_json,
|
||||
session_display_id, last_run_id, last_error, run_count,
|
||||
total_input_tokens, total_output_tokens, rotated_at, rotation_reason,
|
||||
handoff_markdown, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
id, company_id, agent_id, adapter_type, session_params_json,
|
||||
session_display_id, last_run_id, last_error, run_count,
|
||||
total_input_tokens, total_output_tokens, rotated_at, rotation_reason,
|
||||
handoff_markdown, created_at, updated_at
|
||||
FROM task_sessions;
|
||||
DROP TABLE task_sessions;
|
||||
ALTER TABLE task_sessions_new RENAME TO task_sessions;
|
||||
CREATE INDEX IF NOT EXISTS idx_task_sessions_company_agent
|
||||
ON task_sessions(company_id, agent_id, updated_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_sessions_lookup
|
||||
ON task_sessions(company_id, agent_id, adapter_type);
|
||||
`);
|
||||
log.info("[migration v41] rebuilt task_sessions without task_id");
|
||||
}
|
||||
} finally {
|
||||
db.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user