Files
saltbo__agent-kanban/server/usecases/tasks/replaceTaskAssignment.ts
realmroot[bot] e55b740260 fix(tasks): dispatch assignments through Enbor sessions (#271)
* fix(tasks): dispatch assignments through Enbor sessions

* fix(tasks): settle sessions returned after cancellation

* docs(tasks): align agent skill with direct session dispatch

---------

Co-authored-by: jarvis <jarvis@agents.realmroot.dev>
2026-09-05 19:02:55 +00:00

96 lines
3.3 KiB
TypeScript

export interface TaskAssignmentTarget {
version: number;
status: string;
assignedTo: string | null;
assigneeIdentityType: string | null;
activeAssignment: TaskAssignmentCommit | null;
}
export interface TaskAssignmentCommit {
actionId: string;
assignedAt: string;
assignedByActorId: string;
}
export interface TaskAssignmentRepository {
findTarget(ownerId: string, taskId: string): Promise<TaskAssignmentTarget | null>;
create(input: {
ownerId: string;
taskId: string;
assigneeActorId: string;
assignedByActorId: string;
expectedTaskVersion?: number;
}): Promise<TaskAssignmentCommit | null>;
}
export interface TaskAssignment {
id: string;
taskId: string;
agentActorId: string;
assignedByActorId: string;
assignedAt: string;
}
export type TaskAssignmentFailureCode = "TASK_NOT_FOUND" | "TASK_ASSIGNMENT_CONFLICT";
export class TaskAssignmentFailure extends Error {
constructor(
readonly code: TaskAssignmentFailureCode,
message: string,
) {
super(message);
this.name = "TaskAssignmentFailure";
}
}
export async function replaceTaskAssignment(
repository: TaskAssignmentRepository,
input: { ownerId: string; taskId: string; assigneeActorId: string; assignedByActorId: string; expectedTaskVersion?: number },
): Promise<{ assignment: TaskAssignment; version: string; created: boolean }> {
const target = await repository.findTarget(input.ownerId, input.taskId);
if (!target) throw new TaskAssignmentFailure("TASK_NOT_FOUND", "Task not found");
assertTaskVersion(target.version, input.expectedTaskVersion);
if (target.assignedTo === input.assigneeActorId && target.assigneeIdentityType === "realmroot_actor" && target.activeAssignment) {
return assignmentResult(input.taskId, input.assigneeActorId, target.activeAssignment, false);
}
if (target.status !== "todo") {
throw new TaskAssignmentFailure("TASK_ASSIGNMENT_CONFLICT", "Task is not available for assignment");
}
const committed = await repository.create({ ...input, expectedTaskVersion: target.version });
if (!committed) {
const winner = await repository.findTarget(input.ownerId, input.taskId);
if (winner) assertTaskVersion(winner.version, input.expectedTaskVersion);
if (winner?.assignedTo === input.assigneeActorId && winner.assigneeIdentityType === "realmroot_actor" && winner.activeAssignment) {
return assignmentResult(input.taskId, input.assigneeActorId, winner.activeAssignment, false);
}
throw new TaskAssignmentFailure("TASK_ASSIGNMENT_CONFLICT", "Task state or assignment changed before the assignment was committed");
}
return assignmentResult(input.taskId, input.assigneeActorId, committed, true);
}
function assertTaskVersion(actual: number, expected?: number): void {
if (expected !== undefined && actual !== expected) {
throw new TaskAssignmentFailure("TASK_ASSIGNMENT_CONFLICT", "Task changed before the assignment was committed");
}
}
function assignmentResult(
taskId: string,
agentActorId: string,
committed: TaskAssignmentCommit,
created: boolean,
): { assignment: TaskAssignment; version: string; created: boolean } {
return {
assignment: {
id: taskId,
taskId,
agentActorId,
assignedByActorId: committed.assignedByActorId,
assignedAt: committed.assignedAt,
},
version: committed.actionId,
created,
};
}