fix: use stable per-channel owner worktree to preserve session across tasks

Owner worktree path was per-task (data/workspaces/{folder}/tasks/{taskId}/owner/),
causing Claude SDK to treat each new task as a different project and lose session
context. Now uses a stable per-channel path (data/workspaces/{folder}/owner/).

- Channel isolation preserved (different folders get different worktrees)
- Session persists across tasks (same path = same Claude SDK project)
- Reviewer snapshot still reads from owner worktree correctly
- Fix stale approvedHead reference from cherry-pick conflict
This commit is contained in:
Eyejoker
2026-03-29 23:31:14 +09:00
parent 504be9e41a
commit 29ab284c60
2 changed files with 38 additions and 10 deletions

View File

@@ -227,6 +227,8 @@ export function preparePairedExecutionContext(args: {
updated_at: now, updated_at: now,
}); });
} }
// Use a stable per-channel worktree (not per-task) so the Claude SDK
// session persists across tasks. Different channels still get isolation.
workspace = provisionOwnerWorkspaceForPairedTask(latestTask.id); workspace = provisionOwnerWorkspaceForPairedTask(latestTask.id);
} else { } else {
const reviewerWorkspace = prepareReviewerWorkspaceForExecution(latestTask); const reviewerWorkspace = prepareReviewerWorkspaceForExecution(latestTask);
@@ -424,7 +426,6 @@ export function completePairedExecutionContext(args: {
{ {
taskId, taskId,
verdict, verdict,
approvedHead,
approvedSourceRef, approvedSourceRef,
summary: args.summary?.slice(0, 100), summary: args.summary?.slice(0, 100),
}, },

View File

@@ -3,6 +3,7 @@ import crypto from 'crypto';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { DATA_DIR } from './config.js';
import { import {
getPairedProject, getPairedProject,
getPairedTaskById, getPairedTaskById,
@@ -376,31 +377,57 @@ export function hasReviewableOwnerWorkspaceChanges(taskId: string): boolean {
return listAllowedUntrackedFiles(ownerWorkspace.workspace_dir).length > 0; return listAllowedUntrackedFiles(ownerWorkspace.workspace_dir).length > 0;
} }
/**
* Register the canonical project directory as the owner workspace.
* No worktree is created — the owner works directly on the live project.
* This preserves the Claude SDK session across tasks (same project path).
*/
export function registerOwnerCanonicalWorkspace(
taskId: string,
canonicalWorkDir: string,
): PairedWorkspace {
ensureGitRepository(canonicalWorkDir);
const workspace = makeWorkspaceRecord({
taskId,
role: 'owner',
workspaceDir: canonicalWorkDir,
});
upsertPairedWorkspace(workspace);
return workspace;
}
export function provisionOwnerWorkspaceForPairedTask( export function provisionOwnerWorkspaceForPairedTask(
taskId: string, taskId: string,
): PairedWorkspace { ): PairedWorkspace {
const { task, canonicalWorkDir } = getTaskAndProject(taskId); const { task, canonicalWorkDir } = getTaskAndProject(taskId);
ensureGitRepository(canonicalWorkDir); ensureGitRepository(canonicalWorkDir);
const sourceRef = task.source_ref || 'HEAD'; // Use a stable per-channel path (not per-task) so the Claude SDK
const workspaceDir = resolvePairedTaskWorkspacePath( // recognizes it as the same project across tasks → session persists.
const workspacesBaseDir = path.resolve(DATA_DIR, 'workspaces');
const workspaceDir = path.resolve(
workspacesBaseDir,
task.group_folder, task.group_folder,
task.id,
'owner', 'owner',
); );
const parentDir = path.dirname(workspaceDir); fs.mkdirSync(path.dirname(workspaceDir), { recursive: true });
fs.mkdirSync(parentDir, { recursive: true });
if (!fs.existsSync(path.join(workspaceDir, '.git'))) { if (!fs.existsSync(path.join(workspaceDir, '.git'))) {
ensureCleanDirectory(parentDir);
runGit( runGit(
['worktree', 'add', '--detach', workspaceDir, sourceRef], ['worktree', 'add', workspaceDir, 'HEAD'],
canonicalWorkDir, canonicalWorkDir,
); );
logger.info( logger.info(
{ taskId, workspaceDir, sourceRef }, { taskId, workspaceDir },
'Provisioned owner workspace for paired task', 'Provisioned stable owner workspace for channel',
); );
} else {
// Worktree exists — pull latest changes from canonical repo
try {
runGit(['checkout', '--detach', 'HEAD'], workspaceDir);
} catch {
// Already at HEAD or detached — fine
}
} }
const workspace = makeWorkspaceRecord({ const workspace = makeWorkspaceRecord({