feat(coder): re-key agent_sessions to (chat_id, agent) + worktrees table (P1.5-b)
The tab (a chat) is the context unit: two opencode tabs in one session are two independent agent contexts sharing one worktree. agent_sessions re-keys from (session_id, agent) to (chat_id, agent) — chat_id FK ON DELETE CASCADE (closing a tab ends its context); worktree_id and session_id become informational SET NULL columns. New worktrees table (one-per-session, survives session delete via session_id SET NULL) supersedes session_worktrees, which is defanged (CASCADE dropped) not yet removed. chat_id is threaded end-to-end: tasks.chat_id added, written by the coder message + skills routes from the frontend tab, read by runOpenCodeServerTask which falls back to resolve-or-create a chat for session-less creators (arena/MCP/new_task/generic) so ensureSession never gets a null key. Idempotent migration with a backfill-verify gate (0-row assertion after the test session was deleted). config_hash fingerprint logic preserved; one-worktree-per-session unchanged; runExternalAgent untouched. Column rename worktree_path -> path repointed at all five readers (server delete-guard, risk/stash endpoints, ensureSessionWorktree). Supersedes the earlier (worktree_id) draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,8 +78,9 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
mode_id: string | null;
|
||||
thinking_option_id: string | null;
|
||||
session_id: string | null;
|
||||
chat_id: string | null;
|
||||
}[]>`
|
||||
SELECT id, project_id, input, agent, model, mode_id, thinking_option_id, session_id
|
||||
SELECT id, project_id, input, agent, model, mode_id, thinking_option_id, session_id, chat_id
|
||||
FROM tasks
|
||||
WHERE state = 'pending'
|
||||
ORDER BY created_at
|
||||
@@ -110,6 +111,7 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
mode_id: string | null;
|
||||
thinking_option_id: string | null;
|
||||
session_id: string | null;
|
||||
chat_id: string | null;
|
||||
}): Promise<void> {
|
||||
const taskId = task.id;
|
||||
|
||||
@@ -511,6 +513,7 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
mode_id: string | null;
|
||||
thinking_option_id: string | null;
|
||||
session_id: string | null;
|
||||
chat_id: string | null;
|
||||
},
|
||||
installPath: string | null,
|
||||
): Promise<void> {
|
||||
@@ -543,10 +546,18 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
WHERE id = ${taskId}
|
||||
`;
|
||||
|
||||
// Resolve session + chat (mirrors runExternalAgent).
|
||||
// Resolve session + chat. P1.5-b: the chat (tab) is the context key, so the
|
||||
// chat_id MUST be non-null and stable before ensureSession. The coder message
|
||||
// route + skills route stamp task.chat_id with the frontend tab's chat — use
|
||||
// it directly. Session-less creators (arena, MCP, new_task, generic
|
||||
// /api/tasks) leave it null; fall back to resolving/creating a real chat so
|
||||
// ensureSession never receives a degenerate (null, agent) key.
|
||||
let sessionId: string;
|
||||
let chatId: string;
|
||||
if (task.session_id) {
|
||||
if (task.chat_id && task.session_id) {
|
||||
sessionId = task.session_id;
|
||||
chatId = task.chat_id;
|
||||
} else if (task.session_id) {
|
||||
sessionId = task.session_id;
|
||||
const chats = await sql<{ id: string }[]>`
|
||||
SELECT id FROM chats WHERE session_id = ${sessionId} AND status = 'open' ORDER BY created_at DESC LIMIT 1
|
||||
@@ -587,7 +598,7 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
|
||||
// Persistent, session-keyed worktree (shared across turns; NOT torn down
|
||||
// per turn — Phase 3 reaps it). Captures base_commit for a stable diff.
|
||||
const { worktreePath, baseCommit } = await ensureSessionWorktree(sql, projectPath, sessionId, {
|
||||
const { worktreeId, worktreePath, baseCommit } = await ensureSessionWorktree(sql, projectPath, sessionId, {
|
||||
signal: ac.signal,
|
||||
});
|
||||
log.info({ taskId, worktreePath }, 'dispatcher: session worktree ready');
|
||||
@@ -680,7 +691,9 @@ export function createDispatcher(deps: Deps): { start(): void; stop(): Promise<v
|
||||
const handle = await backend.ensureSession(sessionId, {
|
||||
agent,
|
||||
model,
|
||||
chatId,
|
||||
worktreePath,
|
||||
worktreeId,
|
||||
projectId: task.project_id,
|
||||
});
|
||||
const result = await backend.prompt(handle, task.input, {
|
||||
|
||||
Reference in New Issue
Block a user