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:
ZacharyZhang-NY committed 2026-04-18 21:54:31 -04:00
1 parent 6abc1a823f
commit 2ace9183b0
17 files changed
+270 -243

No files matched your search

+2 -4
View File
@@ -224,7 +224,6 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
// 5. Session management: create session and track usage
const sessionId = db.createNewSession({
taskId,
companyId: s.companyId,
agentId: s.agentId,
adapterType: "gemini-cli",
@@ -240,7 +239,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
const task = db.getTask(taskId);
expect(task.costUsd).toBeGreaterThan(0);
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session).not.toBeNull();
expect(session!.totalInputTokens).toBe(5000);
expect(session!.totalOutputTokens).toBe(2000);
@@ -382,7 +381,6 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
// Create session
const sessionId = db.createNewSession({
taskId,
companyId: s.companyId,
agentId: s.agentId,
adapterType: "gemini-cli",
@@ -411,7 +409,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
sessionService.accumulateTokenUsage(sessionId, 3000, 1000, runId2);
// Verify accumulated session state
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session).not.toBeNull();
expect(session!.runCount).toBe(2);
expect(session!.totalInputTokens).toBe(8000);
+1 -2
View File
@@ -442,7 +442,6 @@ describe("Process Recovery -- Context Preservation (D-02)", () => {
// Create a task session (simulating what happens during normal execution)
const sessionId = db.createNewSession({
taskId,
companyId: s.companyId,
agentId: s.agentId,
adapterType: "gemini-cli",
@@ -461,7 +460,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => {
// Get the session and evaluate rotation with maxRuns:1 override
// (so it triggers rotation since we did 1 run)
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session).not.toBeNull();
const rotationResult = sessionService.evaluateRotation(
+7 -15
View File
@@ -116,7 +116,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// Create a session
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -130,7 +129,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
}
// Get the updated session from DB
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session).not.toBeNull();
expect(session!.runCount).toBe(DEFAULT_THRESHOLDS.maxRuns);
@@ -145,7 +144,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -157,7 +155,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// DEFAULT_THRESHOLDS.maxTokens = 500_000 (total input + output)
db.incrementSessionUsage(sessionId, 300000, 250000, "big-run");
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session).not.toBeNull();
const totalTokens = session!.totalInputTokens + session!.totalOutputTokens;
expect(totalTokens).toBe(550000);
@@ -172,7 +170,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -205,7 +202,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
unattributedCost: false,
});
const session = db.getActiveTaskSession(taskId)!;
const session = db.getSessionById(sessionId)!;
// generateHandoff is agent-scoped: it walks the agent's runs across
// all tasks + taskless heartbeats, not a single task.
const handoff = sessionService.generateHandoff(session);
@@ -230,7 +227,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// Create initial session
const oldSessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -246,7 +242,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// Create new session
const newSessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -255,7 +250,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
});
// Get the new active session
const newSession = db.getActiveTaskSession(taskId);
const newSession = db.getSessionById(newSessionId);
expect(newSession).not.toBeNull();
expect(newSession!.id).toBe(newSessionId);
expect(newSession!.id).not.toBe(oldSessionId);
@@ -271,7 +266,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "claude-pro",
@@ -284,7 +278,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
db.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`);
}
const session = db.getActiveTaskSession(taskId)!;
const session = db.getSessionById(sessionId)!;
// Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
const result = sessionService.evaluateRotation(session, "claude-pro");
expect(result.shouldRotate).toBe(true);
@@ -296,7 +290,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "openai-codex",
@@ -309,7 +302,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
db.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`);
}
const session = db.getActiveTaskSession(taskId)!;
const session = db.getSessionById(sessionId)!;
// Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
const result = sessionService.evaluateRotation(session, "openai-codex");
expect(result.shouldRotate).toBe(true);
@@ -322,7 +315,6 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// Create session and add usage
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -352,7 +344,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
});
// Rotate the session (archives old, creates new)
const oldSession = db.getActiveTaskSession(taskId)!;
const oldSession = db.getSessionById(sessionId)!;
sessionService.rotateSession(oldSession);
// After rotation, the handoff should be retrievable for the next run
+20 -41
View File
@@ -95,16 +95,14 @@ describe("task_sessions CRUD", () => {
await rm(tempDir, { recursive: true, force: true });
});
it("getActiveTaskSession returns null when active_session_id is null", () => {
const { taskId } = scaffold(db);
const session = db.getActiveTaskSession(taskId);
it("getSessionById returns null for an unknown id", () => {
const session = db.getSessionById("nonexistent");
expect(session).toBeNull();
});
it("getActiveTaskSession returns TaskSessionRecord when valid session exists", () => {
const { taskId, companyId, agentId } = scaffold(db);
it("getSessionById returns TaskSessionRecord when valid session exists", () => {
const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "claude-pro",
@@ -112,10 +110,9 @@ describe("task_sessions CRUD", () => {
sessionDisplayId: "claude-abc123",
});
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session).not.toBeNull();
expect(session!.id).toBe(sessionId);
expect(session!.taskId).toBe(taskId);
expect(session!.companyId).toBe(companyId);
expect(session!.agentId).toBe(agentId);
expect(session!.adapterType).toBe("claude-pro");
@@ -130,9 +127,8 @@ describe("task_sessions CRUD", () => {
});
it("incrementSessionUsage atomically updates counters", () => {
const { taskId, companyId, agentId } = scaffold(db);
const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "claude-pro",
@@ -142,7 +138,7 @@ describe("task_sessions CRUD", () => {
db.incrementSessionUsage(sessionId, 1000, 500, "run-001");
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session!.runCount).toBe(1);
expect(session!.totalInputTokens).toBe(1000);
expect(session!.totalOutputTokens).toBe(500);
@@ -150,7 +146,7 @@ describe("task_sessions CRUD", () => {
// Second increment accumulates
db.incrementSessionUsage(sessionId, 2000, 800, "run-002");
const session2 = db.getActiveTaskSession(taskId);
const session2 = db.getSessionById(sessionId);
expect(session2!.runCount).toBe(2);
expect(session2!.totalInputTokens).toBe(3000);
expect(session2!.totalOutputTokens).toBe(1300);
@@ -158,9 +154,8 @@ describe("task_sessions CRUD", () => {
});
it("archiveSession sets rotated_at, rotation_reason, handoff_markdown", () => {
const { taskId, companyId, agentId } = scaffold(db);
const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "claude-pro",
@@ -170,17 +165,16 @@ describe("task_sessions CRUD", () => {
db.archiveSession(sessionId, "max_runs", "# Handoff\nSession reached 20 runs.");
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session!.rotatedAt).toBeTruthy();
expect(session!.rotationReason).toBe("max_runs");
expect(session!.handoffMarkdown).toBe("# Handoff\nSession reached 20 runs.");
});
it("createNewSession creates session and updates tasks.active_session_id", () => {
const { taskId, companyId, agentId } = scaffold(db);
it("createNewSession returns an id; adapter type persists", () => {
const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -191,8 +185,7 @@ describe("task_sessions CRUD", () => {
expect(typeof sessionId).toBe("string");
expect(sessionId.length).toBeGreaterThan(0);
// active_session_id should point to new session
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session).not.toBeNull();
expect(session!.id).toBe(sessionId);
expect(session!.adapterType).toBe("gemini-cli");
@@ -297,7 +290,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "gemini-cli",
sessionParamsJson: null,
@@ -323,7 +315,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "gemini-cli",
sessionParamsJson: null,
@@ -349,7 +340,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "gemini-cli",
sessionParamsJson: null,
@@ -376,7 +366,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "gemini-cli",
sessionParamsJson: null,
@@ -407,7 +396,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "claude-pro",
sessionParamsJson: null,
@@ -438,7 +426,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "gemini-cli",
sessionParamsJson: null,
@@ -464,7 +451,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "gemini-cli",
sessionParamsJson: null,
@@ -490,7 +476,6 @@ describe("session management service", () => {
const session: TaskSessionRecord = {
id: "sess-1",
companyId: "co-1",
taskId: "task-1",
agentId: "agent-1",
adapterType: "gemini-cli",
sessionParamsJson: null,
@@ -524,7 +509,6 @@ describe("session management service", () => {
it("accumulateTokenUsage calls db.incrementSessionUsage with correct args", () => {
const { taskId, companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "claude-pro",
@@ -535,7 +519,7 @@ describe("session management service", () => {
svc.accumulateTokenUsage(sessionId, 5000, 2000, "run-abc");
const session = db.getActiveTaskSession(taskId);
const session = db.getSessionById(sessionId);
expect(session!.runCount).toBe(1);
expect(session!.totalInputTokens).toBe(5000);
expect(session!.totalOutputTokens).toBe(2000);
@@ -550,7 +534,6 @@ describe("session management service", () => {
it("generateHandoff produces markdown with task title and run history", () => {
const { taskId, companyId, agentId, workspaceId } = scaffold(db);
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -575,7 +558,7 @@ describe("session management service", () => {
// Increment session usage to have stats
db.incrementSessionUsage(sessionId, 10000, 5000, run.id);
const session = db.getActiveTaskSession(taskId)!;
const session = db.getSessionById(sessionId)!;
const svc = createSessionManagementService({ db, logger: mockLogger });
// Handoff is now agent-scoped: it walks the agent's recent runs
// (across tasks + heartbeats) rather than a single task. The task
@@ -598,7 +581,6 @@ describe("session management service", () => {
it("rotateSession archives old session, creates new, copies session_params", () => {
const { taskId, companyId, agentId } = scaffold(db);
const oldSessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -609,7 +591,7 @@ describe("session management service", () => {
// Add some usage
db.incrementSessionUsage(oldSessionId, 300000, 200000, "run-10");
const oldSession = db.getActiveTaskSession(taskId)!;
const oldSession = db.getSessionById(oldSessionId)!;
const svc = createSessionManagementService({ db, logger: mockLogger });
const newSessionId = svc.rotateSession(oldSession);
@@ -624,8 +606,8 @@ describe("session management service", () => {
expect(archivedRows[0].rotationReason).toBe("threshold_exceeded");
expect(archivedRows[0].handoffMarkdown).toContain("# Session Handoff");
// New session should be active, with fresh counters and copied params
const newSession = db.getActiveTaskSession(taskId)!;
// New session is a distinct row, with fresh counters and copied params
const newSession = db.getSessionById(newSessionId)!;
expect(newSession.id).toBe(newSessionId);
expect(newSession.runCount).toBe(0);
expect(newSession.totalInputTokens).toBe(0);
@@ -645,7 +627,6 @@ describe("session management service", () => {
// Create and rotate a session
const session1Id = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -653,7 +634,7 @@ describe("session management service", () => {
sessionDisplayId: null,
});
db.incrementSessionUsage(session1Id, 100000, 50000, "run-1");
const session1 = db.getActiveTaskSession(taskId)!;
const session1 = db.getSessionById(session1Id)!;
svc.rotateSession(session1);
const handoff = svc.getHandoffContext(taskId);
@@ -664,7 +645,6 @@ describe("session management service", () => {
it("getHandoffContext returns null when no rotated sessions exist", () => {
const { taskId, companyId, agentId } = scaffold(db);
db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -685,7 +665,6 @@ describe("session management service", () => {
it("getUsageDelta returns token difference between snapshot and current totals", () => {
const { taskId, companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({
taskId,
companyId,
agentId,
adapterType: "gemini-cli",
@@ -696,7 +675,7 @@ describe("session management service", () => {
// Pre-populate some usage
db.incrementSessionUsage(sessionId, 1000, 500, "run-1");
const session = db.getActiveTaskSession(taskId)!;
const session = db.getSessionById(sessionId)!;
const svc = createSessionManagementService({ db, logger: mockLogger });
// Snapshot current state