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:
@@ -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),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user