mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-11 02:46:45 +08:00
A worktree-mode local_directory task runs in a disposable git worktree that Finalize removes on every exit path, so the task's `work_dir` pointed at a directory that no longer existed. "Copy local workdir path" handed the user a path they could not `cd` into. The daemon now reports a `durable_work_dir` handoff instead of having clients infer one: - `removeLocalWorktreeDir` returns an error and confirms removal with `Lstat`, so a broken symlink cannot be mistaken for an absent directory. Finalize fails when removal cannot be confirmed. - `durable_work_dir` is written only after Finalize succeeds and the worktree is verifiably gone. A preserved worktree, an in-place run, a running task, a squad-leader task, and any older daemon all leave it unset, keeping the real `work_dir` authoritative. - The field travels on the complete, fail, and cancel-ack callbacks and is persisted on the task row (migration 362, additive nullable column). Writes use COALESCE for replay idempotency; the cancel-ack path CASes on `status = 'cancelled'` so a late ack cannot touch a completed or failed row. - The server derives `relative_durable_work_dir` through the same home-stripping helper as `relative_work_dir`; the absolute value is reserved for explicit clipboard actions and is never rendered. - Clients select the durable path only for a terminal task that carries the field. The issue actions menu and the transcript dialog share one resolver, and the label and toast say the task worktree was reclaimed, naming the delivery branch when one exists. The stored path is a point-in-time delivery snapshot: later renames or detachments of the project resource do not rewrite historical tasks.
69 lines
2.0 KiB
TypeScript
69 lines
2.0 KiB
TypeScript
import type { AgentTask } from "../types/agent";
|
|
|
|
type WorkdirTask = Pick<
|
|
AgentTask,
|
|
| "branch_name"
|
|
| "created_at"
|
|
| "durable_work_dir"
|
|
| "relative_durable_work_dir"
|
|
| "relative_work_dir"
|
|
| "status"
|
|
| "work_dir"
|
|
>;
|
|
|
|
export interface WorkdirCopyTarget {
|
|
path: string;
|
|
relativePath?: string;
|
|
branchName?: string;
|
|
source: "durable_project_directory" | "task_workdir";
|
|
}
|
|
|
|
const terminalStatuses = new Set(["completed", "failed", "cancelled"]);
|
|
|
|
/**
|
|
* Resolves the path copied for a task without inferring ownership from the
|
|
* current machine or mutable project configuration.
|
|
*
|
|
* `durable_work_dir` is a daemon-reported lifecycle fact: it is populated only
|
|
* after a disposable worktree was finalized and its removal confirmed. Until
|
|
* then (and for mixed-version deployments) `work_dir` remains authoritative.
|
|
*/
|
|
export function resolveWorkdirCopyTarget(
|
|
tasks: readonly WorkdirTask[] | undefined,
|
|
): WorkdirCopyTarget | undefined {
|
|
const latestTask = tasks
|
|
?.filter((task) => {
|
|
if (task.work_dir?.trim()) return true;
|
|
return (
|
|
terminalStatuses.has(task.status) &&
|
|
Boolean(task.durable_work_dir?.trim())
|
|
);
|
|
})
|
|
.reduce<WorkdirTask | undefined>(
|
|
(latest, task) =>
|
|
!latest || task.created_at > latest.created_at ? task : latest,
|
|
undefined,
|
|
);
|
|
|
|
if (!latestTask) return undefined;
|
|
|
|
const durablePath = latestTask.durable_work_dir?.trim();
|
|
if (terminalStatuses.has(latestTask.status) && durablePath) {
|
|
return {
|
|
path: durablePath,
|
|
relativePath: latestTask.relative_durable_work_dir?.trim() || undefined,
|
|
branchName: latestTask.branch_name?.trim() || undefined,
|
|
source: "durable_project_directory",
|
|
};
|
|
}
|
|
|
|
const workDir = latestTask.work_dir?.trim();
|
|
if (!workDir) return undefined;
|
|
return {
|
|
path: workDir,
|
|
relativePath: latestTask.relative_work_dir?.trim() || undefined,
|
|
branchName: latestTask.branch_name?.trim() || undefined,
|
|
source: "task_workdir",
|
|
};
|
|
}
|