fix(FN-7952): finish async workflow selection cutover

Use PostgreSQL workflow selections in the dashboard TUI, authoritative driver, and graph-runner adapter so migrated tasks cannot silently fall back to the coding workflow.
This commit is contained in:
gsxdsm
2026-07-14 17:08:29 -07:00
parent 2d61976df0
commit 6aff4958ad
3 changed files with 33 additions and 8 deletions

View File

@@ -1031,7 +1031,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
): Promise<ResolvedColumnInfo> {
if (!flagOn) return {};
try {
const selection = projectStore.getTaskWorkflowSelection(task.id);
/*
FNXC:WorkflowSelection 2026-07-14-17:06:
The dashboard TUI must resolve task workflow selections through the asynchronous store API so PostgreSQL-backed projects retain custom workflow column names and trait flags. The synchronous compatibility method has no backend result and is reserved for legacy test doubles.
*/
const selection = await projectStore.getTaskWorkflowSelectionAsync(task.id);
const workflowId = selection?.workflowId;
let columns = workflowIrCache.get(workflowId);
if (columns === undefined) {
@@ -3175,7 +3179,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
try {
const values = (t as { customFields?: Record<string, unknown> }).customFields;
if (values && Object.keys(values).length > 0) {
const selection = projectStore.getTaskWorkflowSelection(t.id);
/*
FNXC:WorkflowSelection 2026-07-14-17:06:
Task-detail custom-field chips must use the asynchronous workflow-selection read so PostgreSQL tasks render fields declared by their selected workflow.
*/
const selection = await projectStore.getTaskWorkflowSelectionAsync(t.id);
const def = selection?.workflowId
? await projectStore.getWorkflowDefinition(selection.workflowId)
: undefined;

View File

@@ -5135,7 +5135,10 @@ export class TaskExecutor {
*/
settings = { ...settings };
let selection: { workflowId: string; stepIds: string[] } | undefined;
if (typeof this.store.getTaskWorkflowSelection !== "function") {
if (
typeof this.store.getTaskWorkflowSelectionAsync !== "function"
&& typeof this.store.getTaskWorkflowSelection !== "function"
) {
/*
FNXC:WorkflowExecution 2026-06-23-22:01:
Graph execution is the default for production TaskStore implementations, which expose workflow-selection APIs. Minimal test stores and older embedded adapters can lack that API; fall back to the legacy executor instead of half-entering graph routing with no workflow persistence surface.
@@ -5159,7 +5162,7 @@ export class TaskExecutor {
disposition: "failed",
outcome: "failure",
reason:
"workflow-selection-api-unavailable: store lacks getTaskWorkflowSelection so the workflow graph cannot run "
"workflow-selection-api-unavailable: store lacks a workflow-selection reader so the workflow graph cannot run "
+ `${gateTask.enabledWorkflowSteps?.length ?? 0} enabled pre-merge workflow step(s); the legacy runWorkflowSteps path was removed (U4). Failing closed rather than skipping gates (KTD-5).`,
visitedNodeIds: [],
});
@@ -5249,8 +5252,12 @@ export class TaskExecutor {
const runner = new WorkflowGraphTaskRunner({
store: {
...this.store,
getTaskWorkflowSelection: (taskId: string) =>
this.store.getTaskWorkflowSelection?.(taskId) ?? { workflowId: "builtin:coding", stepIds: [] },
/*
FNXC:WorkflowSelection 2026-07-14-17:06:
Graph execution must reuse the asynchronously resolved selection. A PostgreSQL TaskStore cannot provide that selection through the synchronous compatibility method, and substituting builtin:coding here would silently execute the wrong graph.
*/
getTaskWorkflowSelection: () => selection,
getTaskWorkflowSelectionAsync: async () => selection,
getWorkflowDefinition: async (id: string) =>
(await this.store.getWorkflowDefinition?.(id))
?? (id === "builtin:coding" ? getBuiltinWorkflow("builtin:coding") : undefined),
@@ -5940,7 +5947,10 @@ export class TaskExecutor {
*/
private async maybeObserveWorkflowParity(taskId: string, settings: Settings): Promise<void> {
if (!isExperimentalFeatureEnabled(settings, WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG)) return;
if (typeof this.store.getTaskWorkflowSelection !== "function") return;
if (
typeof this.store.getTaskWorkflowSelectionAsync !== "function"
&& typeof this.store.getTaskWorkflowSelection !== "function"
) return;
try {
const selection = typeof this.store.getTaskWorkflowSelectionAsync === "function"
? await this.store.getTaskWorkflowSelectionAsync(taskId)

View File

@@ -22,6 +22,7 @@ export interface WorkflowAuthoritativeDriverStore {
getSettings(): Promise<Settings>;
getTask(taskId: string): Promise<TaskDetail>;
getTaskWorkflowSelection?(taskId: string): { workflowId: string; stepIds: string[] } | undefined;
getTaskWorkflowSelectionAsync?(taskId: string): Promise<{ workflowId: string; stepIds: string[] } | undefined>;
getWorkflowParitySummary?(options?: { since?: string; limit?: number }): WorkflowParitySummary;
}
@@ -160,7 +161,13 @@ export class WorkflowAuthoritativeDriver {
};
}
const existingSelection = this.deps.store.getTaskWorkflowSelection?.(task.id);
/*
FNXC:WorkflowSelection 2026-07-14-17:06:
The authoritative cutover guard must await PostgreSQL workflow-selection storage before deciding a task is unselected. Keep the synchronous branch only for narrow legacy/in-memory driver doubles.
*/
const existingSelection = this.deps.store.getTaskWorkflowSelectionAsync
? await this.deps.store.getTaskWorkflowSelectionAsync(task.id)
: this.deps.store.getTaskWorkflowSelection?.(task.id);
if (existingSelection) {
return {
handled: false,