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,
});
}
// 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);
} else {
const reviewerWorkspace = prepareReviewerWorkspaceForExecution(latestTask);
@@ -424,7 +426,6 @@ export function completePairedExecutionContext(args: {
{
taskId,
verdict,
approvedHead,
approvedSourceRef,
summary: args.summary?.slice(0, 100),
},

View File

@@ -3,6 +3,7 @@ import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { DATA_DIR } from './config.js';
import {
getPairedProject,
getPairedTaskById,
@@ -376,31 +377,57 @@ export function hasReviewableOwnerWorkspaceChanges(taskId: string): boolean {
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(
taskId: string,
): PairedWorkspace {
const { task, canonicalWorkDir } = getTaskAndProject(taskId);
ensureGitRepository(canonicalWorkDir);
const sourceRef = task.source_ref || 'HEAD';
const workspaceDir = resolvePairedTaskWorkspacePath(
// Use a stable per-channel path (not per-task) so the Claude SDK
// 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.id,
'owner',
);
const parentDir = path.dirname(workspaceDir);
fs.mkdirSync(parentDir, { recursive: true });
fs.mkdirSync(path.dirname(workspaceDir), { recursive: true });
if (!fs.existsSync(path.join(workspaceDir, '.git'))) {
ensureCleanDirectory(parentDir);
runGit(
['worktree', 'add', '--detach', workspaceDir, sourceRef],
['worktree', 'add', workspaceDir, 'HEAD'],
canonicalWorkDir,
);
logger.info(
{ taskId, workspaceDir, sourceRef },
'Provisioned owner workspace for paired task',
{ taskId, workspaceDir },
'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({