feat(FN-5129): complete Step 2 — enforce lineage gate in deleteTask

Fusion-Task-Id: FN-5129
Fusion-Task-Lineage: 77519a37-0552-4e8b-8061-ce84012de978
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 06:14:08 -07:00
committed by gsxdsm
parent 911125f1c9
commit 5d9669ad35

View File

@@ -5718,7 +5718,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
*/
async deleteTask(
id: string,
options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction },
options?: {
removeDependencyReferences?: boolean;
removeLineageReferences?: boolean;
githubIssueAction?: GithubIssueAction;
},
): Promise<Task> {
return this.withTaskLock(id, async () => {
// Flush buffered agent logs inside the lock so no new appends for this
@@ -5741,6 +5745,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
throw new TaskHasDependentsError(id, dependentIds);
}
// FN-5127: lineage gate must execute after idempotent short-circuit.
const lineageChildIds = this.findLiveLineageChildren(id);
if (lineageChildIds.length > 0 && !options?.removeLineageReferences) {
throw new TaskHasLineageChildrenError(id, lineageChildIds);
}
// Clean up the task's branch before deleting from DB
const cleanedBranches = await this.cleanupBranchForTask(task);
if (cleanedBranches.length > 0) {
@@ -5752,8 +5762,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
let rewrittenDependents: Task[] = [];
let rewrittenLineageChildren: Task[] = [];
this.db.transaction(() => {
rewrittenDependents = this.rewriteDependentsForRemoval(id, dependentIds);
rewrittenLineageChildren = this.rewriteLineageChildrenForRemoval(id, lineageChildIds);
const deletedAt = new Date().toISOString();
this.db.prepare("UPDATE tasks SET deletedAt = ?, updatedAt = ? WHERE id = ?").run(deletedAt, deletedAt, id);
this.clearLinkedAgentTaskIds(id, deletedAt);
@@ -5766,6 +5778,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
for (const dependentTask of rewrittenDependents) {
this.emit("task:updated", dependentTask);
}
for (const lineageChild of rewrittenLineageChildren) {
this.emit("task:updated", lineageChild);
}
const linkedFeature = this.missionStore?.getFeatureByTaskId(id);
if (linkedFeature) {
@@ -5815,6 +5830,29 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return rewrittenDependents;
}
private rewriteLineageChildrenForRemoval(parentId: string, childIds: string[]): Task[] {
const rewrittenChildren: Task[] = [];
for (const childId of childIds) {
const childTask = this.readTaskFromDb(childId);
if (!childTask || childTask.sourceParentTaskId !== parentId) continue;
const updatedChild: Task = {
...childTask,
sourceParentTaskId: undefined,
updatedAt: new Date().toISOString(),
};
this.db.prepare("UPDATE tasks SET sourceParentTaskId = NULL, updatedAt = ? WHERE id = ?").run(updatedChild.updatedAt, updatedChild.id);
if (this.isWatching) {
this.taskCache.set(updatedChild.id, updatedChild);
}
rewrittenChildren.push(updatedChild);
}
return rewrittenChildren;
}
/**
* Clear `agent.taskId` links that point at a task which has transitioned out
* of active work. This keeps heartbeat scheduling aligned with live task