Files
saltbo__agent-kanban/server/usecases/tasks/replaceTaskCancellation.ts
jarvis a2fa54a729 feat!: rebuild Agent Kanban for Realmroot Toolbox
Replace the v1 Agent/Machine runtime and CLI architecture with the v2 Resource Server, AMA-backed projections, task lifecycle, session observation, BDD specifications, and a single-package layered codebase.

BREAKING CHANGE: remove the v1 CLI, internal Agent/Machine runtime entities, workspace layout, and legacy runtime coordination APIs.
2026-09-02 00:17:52 -04:00

93 lines
3.0 KiB
TypeScript

export type TaskCancellationActor = { type: "agent" | "human" | "system"; id: string };
export interface StoredTaskCancellation {
actionId: string;
actorType: "agent" | "human" | "system";
actorId: string;
cancelledAt: string;
assigneeActorId: string | null;
}
export interface TaskCancellationTarget {
status: string;
assignedTo: string | null;
assigneeIdentityType: string | null;
cancellation: StoredTaskCancellation | null;
}
export interface TaskCancellationRepository {
findTarget(ownerId: string, taskId: string): Promise<TaskCancellationTarget | null>;
create(input: {
ownerId: string;
taskId: string;
actor: TaskCancellationActor;
assigneeActorId: string | null;
}): Promise<StoredTaskCancellation | null>;
}
export interface TaskCancellation {
id: string;
taskId: string;
cancelledByActorType: "agent" | "human" | "system";
cancelledByActorId: string;
cancelledAt: string;
}
export type TaskCancellationFailureCode = "TASK_NOT_FOUND" | "TASK_CANCELLATION_CONFLICT";
export class TaskCancellationFailure extends Error {
constructor(
readonly code: TaskCancellationFailureCode,
message: string,
) {
super(message);
this.name = "TaskCancellationFailure";
}
}
export async function replaceTaskCancellation(
repository: TaskCancellationRepository,
input: { ownerId: string; taskId: string; actor: TaskCancellationActor },
): Promise<{ cancellation: TaskCancellation; version: string; created: boolean; assigneeActorId: string | null }> {
let target = await repository.findTarget(input.ownerId, input.taskId);
if (!target) throw new TaskCancellationFailure("TASK_NOT_FOUND", "Task not found");
let assigneeActorId = target.cancellation?.assigneeActorId ?? (target.assigneeIdentityType === "realmroot_actor" ? target.assignedTo : null);
let cancellation = target.cancellation;
let created = false;
if (!cancellation) {
if (!isCancellable(target.status)) {
throw new TaskCancellationFailure("TASK_CANCELLATION_CONFLICT", `Task cannot be cancelled from ${target.status}`);
}
cancellation = await repository.create({ ...input, assigneeActorId });
if (!cancellation) {
target = await repository.findTarget(input.ownerId, input.taskId);
if (!target) throw new TaskCancellationFailure("TASK_NOT_FOUND", "Task not found");
cancellation = target.cancellation;
if (!cancellation) {
throw new TaskCancellationFailure("TASK_CANCELLATION_CONFLICT", "Task state changed before the cancellation was committed");
}
assigneeActorId = cancellation.assigneeActorId;
} else {
created = true;
}
}
return {
cancellation: {
id: input.taskId,
taskId: input.taskId,
cancelledByActorType: cancellation.actorType,
cancelledByActorId: cancellation.actorId,
cancelledAt: cancellation.cancelledAt,
},
version: cancellation.actionId,
created,
assigneeActorId,
};
}
function isCancellable(status: string): boolean {
return status === "todo" || status === "in_progress" || status === "in_review";
}