feat(FN-3450): add mesh task create replication across nodes
Implements distributed mesh task creation by adding replicated create primitives to the core store, wiring new API routes (`register-mesh-routes.ts`) that replicate task creation across clustered nodes while preserving remote-targeting metadata, and updating the dashboard's task creation flow accord Fusion-Task-Id: FN-3450
This commit is contained in:
169
packages/core/src/mesh-task-replication.ts
Normal file
169
packages/core/src/mesh-task-replication.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { MeshReplicatedTaskCreatePayload, Task, TaskCreateInput, TaskDetail, TaskSource } from "./types.js";
|
||||
|
||||
export function buildBootstrapPrompt(taskId: string, title: string | undefined, description: string): string {
|
||||
const heading = title ? `${taskId}: ${title}` : taskId;
|
||||
return `# ${heading}\n\n${description}\n`;
|
||||
}
|
||||
|
||||
export function buildMeshReplicatedTaskCreatePayload(input: {
|
||||
taskId: string;
|
||||
reservationId: string;
|
||||
sourceNodeId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
prompt: string;
|
||||
createInput: TaskCreateInput;
|
||||
}): MeshReplicatedTaskCreatePayload {
|
||||
return {
|
||||
replicationVersion: 1,
|
||||
reservationId: input.reservationId,
|
||||
taskId: input.taskId,
|
||||
sourceNodeId: input.sourceNodeId,
|
||||
createdAt: input.createdAt,
|
||||
updatedAt: input.updatedAt,
|
||||
prompt: input.prompt,
|
||||
input: input.createInput,
|
||||
};
|
||||
}
|
||||
|
||||
function pruneUndefined<T>(value: T): T {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => pruneUndefined(entry)) as T;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const entries = Object.entries(value as Record<string, unknown>)
|
||||
.filter(([, entry]) => entry !== undefined)
|
||||
.map(([key, entry]) => [key, pruneUndefined(entry)]);
|
||||
return Object.fromEntries(entries) as T;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeCreateInput(input: TaskCreateInput): TaskCreateInput {
|
||||
const source = input.source;
|
||||
return pruneUndefined({
|
||||
...input,
|
||||
column: input.column ?? "triage",
|
||||
source: source
|
||||
? {
|
||||
...source,
|
||||
sourceType: source.sourceType ?? "unknown",
|
||||
}
|
||||
: { sourceType: "unknown" as const },
|
||||
dependencies: input.dependencies ?? [],
|
||||
enabledWorkflowSteps: input.enabledWorkflowSteps ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
function toTaskSource(source: Omit<TaskSource, "sourceType"> & { sourceType?: TaskSource["sourceType"] }): TaskSource {
|
||||
return {
|
||||
...source,
|
||||
sourceType: source.sourceType ?? "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
function isSubsetEqual(expected: unknown, actual: unknown): boolean {
|
||||
if (Array.isArray(expected)) {
|
||||
return Array.isArray(actual)
|
||||
&& expected.length === actual.length
|
||||
&& expected.every((entry, index) => isSubsetEqual(entry, actual[index]));
|
||||
}
|
||||
if (expected && typeof expected === "object") {
|
||||
if (!actual || typeof actual !== "object") return false;
|
||||
const expectedRecord = expected as Record<string, unknown>;
|
||||
const actualRecord = actual as Record<string, unknown>;
|
||||
return Object.entries(expectedRecord).every(([key, value]) => isSubsetEqual(value, actualRecord[key]));
|
||||
}
|
||||
return Object.is(expected, actual);
|
||||
}
|
||||
|
||||
export function taskMatchesReplicatedCreate(existing: TaskDetail, payload: MeshReplicatedTaskCreatePayload): boolean {
|
||||
const existingPrompt = existing.prompt;
|
||||
const existingCreateInput: TaskCreateInput = {
|
||||
title: existing.title,
|
||||
description: existing.description,
|
||||
column: existing.column,
|
||||
dependencies: existing.dependencies,
|
||||
breakIntoSubtasks: existing.breakIntoSubtasks,
|
||||
enabledWorkflowSteps: existing.enabledWorkflowSteps,
|
||||
modelPresetId: existing.modelPresetId,
|
||||
modelProvider: existing.modelProvider,
|
||||
modelId: existing.modelId,
|
||||
validatorModelProvider: existing.validatorModelProvider,
|
||||
validatorModelId: existing.validatorModelId,
|
||||
planningModelProvider: existing.planningModelProvider,
|
||||
planningModelId: existing.planningModelId,
|
||||
thinkingLevel: existing.thinkingLevel,
|
||||
missionId: existing.missionId,
|
||||
sliceId: existing.sliceId,
|
||||
assignedAgentId: existing.assignedAgentId,
|
||||
nodeId: existing.nodeId,
|
||||
assigneeUserId: existing.assigneeUserId,
|
||||
reviewLevel: existing.reviewLevel,
|
||||
executionMode: existing.executionMode,
|
||||
priority: existing.priority,
|
||||
sourceIssue: existing.sourceIssue,
|
||||
source: toTaskSource({
|
||||
sourceType: existing.sourceType,
|
||||
sourceAgentId: existing.sourceAgentId,
|
||||
sourceRunId: existing.sourceRunId,
|
||||
sourceSessionId: existing.sourceSessionId,
|
||||
sourceMessageId: existing.sourceMessageId,
|
||||
sourceParentTaskId: existing.sourceParentTaskId,
|
||||
sourceMetadata: existing.sourceMetadata,
|
||||
}),
|
||||
baseBranch: existing.baseBranch,
|
||||
branch: existing.branch,
|
||||
};
|
||||
|
||||
return (
|
||||
existing.id === payload.taskId &&
|
||||
existing.createdAt === payload.createdAt &&
|
||||
existing.updatedAt === payload.updatedAt &&
|
||||
existingPrompt === payload.prompt &&
|
||||
isSubsetEqual(normalizeCreateInput(payload.input), normalizeCreateInput(existingCreateInput))
|
||||
);
|
||||
}
|
||||
|
||||
export function replicationCollisionError(taskId: string): Error {
|
||||
return new Error(`Replicated task payload collision for existing task ${taskId}`);
|
||||
}
|
||||
|
||||
export function toReplicatedCreateInput(task: Task): TaskCreateInput {
|
||||
return {
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
column: task.column,
|
||||
dependencies: task.dependencies,
|
||||
breakIntoSubtasks: task.breakIntoSubtasks,
|
||||
enabledWorkflowSteps: task.enabledWorkflowSteps,
|
||||
modelPresetId: task.modelPresetId,
|
||||
modelProvider: task.modelProvider,
|
||||
modelId: task.modelId,
|
||||
validatorModelProvider: task.validatorModelProvider,
|
||||
validatorModelId: task.validatorModelId,
|
||||
planningModelProvider: task.planningModelProvider,
|
||||
planningModelId: task.planningModelId,
|
||||
thinkingLevel: task.thinkingLevel,
|
||||
missionId: task.missionId,
|
||||
sliceId: task.sliceId,
|
||||
assignedAgentId: task.assignedAgentId,
|
||||
nodeId: task.nodeId,
|
||||
assigneeUserId: task.assigneeUserId,
|
||||
reviewLevel: task.reviewLevel,
|
||||
executionMode: task.executionMode,
|
||||
priority: task.priority,
|
||||
sourceIssue: task.sourceIssue,
|
||||
source: toTaskSource({
|
||||
sourceType: task.sourceType,
|
||||
sourceAgentId: task.sourceAgentId,
|
||||
sourceRunId: task.sourceRunId,
|
||||
sourceSessionId: task.sourceSessionId,
|
||||
sourceMessageId: task.sourceMessageId,
|
||||
sourceParentTaskId: task.sourceParentTaskId,
|
||||
sourceMetadata: task.sourceMetadata,
|
||||
}),
|
||||
baseBranch: task.baseBranch,
|
||||
branch: task.branch,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user