Extend workspace lifecycle behavior so mono-repository and multi-repository tasks share the same durable scope, workflow, review, and merge semantics. - Add repository-scope persistence, migrations, serialization, and task mutation support. - Align dashboard task creation, detail, and workspace worktree views with repository-aware lifecycle state. - Extend executor, reviewer, merger, triage, and self-healing paths with parity coverage and tests. - Document the lifecycle contract and publish the operator-facing changeset. Files changed: .changeset/fn-094-multi-repository-lifecycle.md | 7 + docs/agents.md | 8 + docs/architecture.md | 6 + docs/dashboard-guide.md | 4 + docs/multi-project.md | 6 + docs/settings-reference.md | 2 +- docs/testing.md | 12 ++ docs/workspaces.md | 6 + ...workspace-worktrees-concurrent-merge.pg.test.ts | 19 +++ packages/core/src/index.ts | 2 +- .../core/src/postgres/migrations/0000_initial.sql | 1 + .../0064_fn_094_task_repository_scope.sql | 2 + packages/core/src/postgres/schema-applier.ts | 13 +- packages/core/src/postgres/schema/project.ts | 2 + packages/core/src/store.ts | 21 ++- .../core/src/task-store/branch-and-pr-entities.ts | 2 +- packages/core/src/task-store/persistence.ts | 4 +- packages/core/src/task-store/serialization.ts | 2 + packages/core/src/task-store/task-creation.ts | 31 ++++ packages/core/src/task-store/task-mutation-ops.ts | 131 +++++++++++++++- packages/core/src/task-store/task-row-mappers.ts | 2 +- packages/core/src/task-store/task-update.ts | 55 +++++-- packages/core/src/tasks/repository-scope.ts | 29 ++++ packages/core/src/types.ts | 4 + packages/core/src/types/task/task-core.ts | 35 ++++- packages/core/src/types/workflow/workflow-steps.ts | 21 +++ packages/dashboard/app/api/tasks/tasks.ts | 14 ++ packages/dashboard/app/components/NewTaskModal.tsx | 16 +- .../dashboard/app/components/TaskDetailModal.css | 27 ++++ .../dashboard/app/components/TaskDetailModal.tsx | 15 +- .../app/components/WorkspaceWorktreesSummary.tsx | 68 ++++++++- .../app/components/__tests__/NewTaskModal.test.tsx | 1 + .../__tests__/WorkspaceWorktreesSummary.test.tsx | 27 +++- .../src/routes/register-task-workflow-routes.ts | 48 ++++++ .../__tests__/executor-workspace-taskdone.test.ts | 20 +++ .../src/__tests__/reviewer-workspace.test.ts | 170 ++++++++++++++++++++- .../src/__tests__/self-healing-workspace.test.ts | 4 + .../workflow-graph-optional-group.test.ts | 32 ++++ .../workflow-graph-optional-step-fix.test.ts | 58 +++++++ .../engine/src/__tests__/workspace-e2e.test.ts | 91 ++++++++++- .../__tests__/workspace-lifecycle-parity.test.ts | 61 ++++++++ .../src/__tests__/workspace-merger-lease.test.ts | 25 +++ .../__tests__/workspace-merger-scope-gates.test.ts | 58 +++++++ .../engine/src/__tests__/workspace-merger.test.ts | 59 ++++++- packages/engine/src/agent-tools.ts | 92 ++++++++++- packages/engine/src/execution/reviewer.ts | 10 +- .../create-authoritative-workflow-seams.ts | 95 ++++++++++-- .../engine/src/executor/execute-workflow-graph.ts | 37 ++++- .../request-pre-merge-optional-step-fix.ts | 70 +++++++++ .../engine/src/executor/run-graph-custom-node.ts | 124 ++++++++++----- .../engine/src/executor/workflow-step-verdict.ts | 6 +- .../src/executor/workspace-review-per-repo.ts | 118 +++++++++++++- .../src/executor/worktree-task-done-scope-leak.ts | 109 ++++++++++++- packages/engine/src/merge/merger-ai.ts | 88 ++++++++++- packages/engine/src/self-healing.ts | 19 ++- packages/engine/src/triage.ts | 9 ++ .../src/workflows/workflow-graph-executor.ts | 73 +++++++-- .../engine/src/workflows/workflow-node-handlers.ts | 12 +- 58 files changed, 1962 insertions(+), 121 deletions(-) Fusion-Task-Id: FN-094 Fusion-Task-Lineage: 483f958f-cc67-474e-9383-26856c329543 Co-authored-by: Fusion <noreply@runfusion.ai>
443 lines
25 KiB
TypeScript
443 lines
25 KiB
TypeScript
/**
|
|
* Task persistence row shape, column descriptors, and serialization SQL.
|
|
*
|
|
* FNXC:TaskStoreDecompose 2026-06-24-00:00:
|
|
* Extracted from the monolithic packages/core/src/store.ts (U5 decomposition).
|
|
* Pure behavior-invariant move: types and constants are byte-identical to their
|
|
* pre-extraction form. store.ts re-imports these symbols. The descriptor order
|
|
* stays in lockstep with the named-column INSERT/UPSERT clauses generated below.
|
|
*/
|
|
import type { Task } from "../types.js";
|
|
import { normalizeTaskPriority } from "../tasks/task-priority.js";
|
|
import { toJson, toJsonNullable, fromJson} from "../db/db.js";
|
|
|
|
/** Database row shape for the tasks table (all columns). */
|
|
export interface TaskRow {
|
|
id: string;
|
|
lineageId: string | null;
|
|
title: string | null;
|
|
description: string;
|
|
priority: string | null;
|
|
column: string;
|
|
status: string | null;
|
|
size: string | null;
|
|
reviewLevel: number | null;
|
|
currentStep: number;
|
|
worktree: string | null;
|
|
blockedBy: string | null;
|
|
overlapBlockedBy: string | null;
|
|
queuedLogEpisodeSignature: string | null;
|
|
paused: number | null;
|
|
pausedReason: string | null;
|
|
wedgeNotification: string | null;
|
|
userPaused: number | null;
|
|
baseBranch: string | null;
|
|
executionStartBranch: string | null;
|
|
branch: string | null;
|
|
autoMerge: number | null;
|
|
autoMergeProvenance: string | null;
|
|
baseCommitSha: string | null;
|
|
modelPresetId: string | null;
|
|
modelProvider: string | null;
|
|
credentialInstanceId: string | null;
|
|
modelId: string | null;
|
|
validatorModelProvider: string | null;
|
|
validatorCredentialInstanceId: string | null;
|
|
validatorModelId: string | null;
|
|
planningModelProvider: string | null;
|
|
planningCredentialInstanceId: string | null;
|
|
planningModelId: string | null;
|
|
mergerModelProvider: string | null;
|
|
mergerCredentialInstanceId: string | null;
|
|
mergerModelId: string | null;
|
|
mergeRetries: number | null;
|
|
aiMergeReviewReconciliation: string | null;
|
|
workflowStepRetries: number | null;
|
|
stuckKillCount: number | null;
|
|
resumeLimboCount: number | null;
|
|
graphResumeRetryCount: number | null;
|
|
consecutiveToolFailureRetryCount: number | null;
|
|
executorEscalationAttempted: number | null;
|
|
toolFailureDetectorLogCursor: number | null;
|
|
toolFailureRetryExhaustedAuditEmitted: number | null;
|
|
resumeLimboTipSha: string | null;
|
|
resumeLimboStepSignature: string | null;
|
|
executeRequeueLoopCount: number | null;
|
|
executeRequeueLoopSignature: string | null;
|
|
postReviewFixCount: number | null;
|
|
planReviewReplanCount: number | null;
|
|
recoveryRetryCount: number | null;
|
|
taskDoneRetryCount: number | null;
|
|
// FNXC:Lifecycle 2026-07-16-21:40: FN-8141 skip-bypass taint marker (ISO timestamp / null).
|
|
bulkCompletionRefusalAt: string | null;
|
|
// FNXC:WorkflowIrPin 2026-07-19-03:10: U9b/KTD-3 durable IR pin + the node entry it belongs to.
|
|
workflowIrPin: string | null;
|
|
workflowIrPinNodeId: string | null;
|
|
workflowIrPinColumnId: string | null;
|
|
// FNXC:LegacyAdoption 2026-07-19-03:10: U9b/KTD-8 one-time adoption stamp (ISO timestamp / null).
|
|
legacyAdoptedAt: string | null;
|
|
worktreeSessionRetryCount: number | null;
|
|
completionHandoffLimboRecoveryCount: number | null;
|
|
verificationFailureCount: number | null;
|
|
mergeConflictBounceCount: number | null;
|
|
mergeAuditBounceCount: number | null;
|
|
mergeTransientRetryCount: number | null;
|
|
branchConflictRecoveryCount: number | null;
|
|
reviewerContextRetryCount: number | null;
|
|
reviewerFallbackRetryCount: number | null;
|
|
nextRecoveryAt: string | null;
|
|
error: string | null;
|
|
summary: string | null;
|
|
recommendations: string | null;
|
|
thinkingLevel: string | null;
|
|
validatorThinkingLevel: string | null;
|
|
planningThinkingLevel: string | null;
|
|
mergerThinkingLevel: string | null;
|
|
executionMode: string | null;
|
|
/** FNXC:PlannerOversight 2026-07-14-18:11: null = inherit project; 0 = off; 1 = on (autoMerge pattern). */
|
|
sessionAdvisorEnabled: number | null;
|
|
tokenUsageInputTokens: number | null;
|
|
tokenUsageOutputTokens: number | null;
|
|
tokenUsageCachedTokens: number | null;
|
|
tokenUsageCacheWriteTokens: number | null;
|
|
tokenUsageTotalTokens: number | null;
|
|
tokenUsageFirstUsedAt: string | null;
|
|
tokenUsageLastUsedAt: string | null;
|
|
tokenUsageModelProvider: string | null;
|
|
tokenUsageModelId: string | null;
|
|
tokenUsagePerModel: string | null;
|
|
tokenBudgetSoftAlertedAt: string | null;
|
|
tokenBudgetHardAlertedAt: string | null;
|
|
tokenBudgetOverride: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
columnMovedAt: string | null;
|
|
firstExecutionAt: string | null;
|
|
cumulativeActiveMs: number | null;
|
|
cumulativePlanningMs: number | null;
|
|
planningStartedAt: string | null;
|
|
columnDwellMs: string | null;
|
|
workflowTransitionNotification: string | null;
|
|
plannerOversightLevel: string | null;
|
|
awaitingApprovalReason: string | null;
|
|
approvedPlanFingerprint: string | null;
|
|
executionStartedAt: string | null;
|
|
executionCompletedAt: string | null;
|
|
dependencies: string | null;
|
|
steps: string | null;
|
|
customFields: string | null;
|
|
log: string | null;
|
|
attachments: string | null;
|
|
steeringComments: string | null;
|
|
comments: string | null;
|
|
review: string | null;
|
|
reviewState: string | null;
|
|
workflowStepResults: string | null;
|
|
prInfo: string | null;
|
|
prInfos: string | null;
|
|
issueInfo: string | null;
|
|
githubTracking: string | null;
|
|
gitlabTracking: string | null;
|
|
sourceIssueProvider: string | null;
|
|
sourceIssueRepository: string | null;
|
|
sourceIssueExternalIssueId: string | null;
|
|
sourceIssueNumber: number | null;
|
|
sourceIssueUrl: string | null;
|
|
sourceIssueClosedAt: string | null;
|
|
mergeDetails: string | null;
|
|
workspaceWorktrees: string | null;
|
|
repositoryScope: string | null;
|
|
noCommitsExpected: number | null;
|
|
enabledWorkflowSteps: string | null;
|
|
modifiedFiles: string | null;
|
|
declaredSymbols: string | null;
|
|
missionId: string | null;
|
|
sliceId: string | null;
|
|
scopeOverride: number | null;
|
|
scopeOverrideReason: string | null;
|
|
scopeAutoWiden: string | null;
|
|
assignedAgentId: string | null;
|
|
pausedByAgentId: string | null;
|
|
assigneeUserId: string | null;
|
|
nodeId: string | null;
|
|
effectiveNodeId: string | null;
|
|
effectiveNodeSource: string | null;
|
|
sourceType: string | null;
|
|
sourceAgentId: string | null;
|
|
sourceRunId: string | null;
|
|
sourceSessionId: string | null;
|
|
sourceMessageId: string | null;
|
|
sourceParentTaskId: string | null;
|
|
sourceMetadata: string | null;
|
|
proposalClaimId: string | null;
|
|
checkedOutBy: string | null;
|
|
checkedOutAt: string | null;
|
|
checkoutNodeId: string | null;
|
|
checkoutRunId: string | null;
|
|
checkoutLeaseRenewedAt: string | null;
|
|
checkoutLeaseEpoch: number | null;
|
|
deletedAt: string | null;
|
|
allowResurrection: number | null;
|
|
}
|
|
|
|
export type TaskPersistSerializationContext = {
|
|
lineageId: string;
|
|
};
|
|
|
|
export type TaskColumnDescriptor = {
|
|
column: keyof TaskRow;
|
|
sqlIdentifier: string;
|
|
serialize: (task: Task, context: TaskPersistSerializationContext) => unknown;
|
|
};
|
|
|
|
/**
|
|
* FNXC:TaskStateReconciliation 2026-07-29-20:53:
|
|
* A generic PostgreSQL task write may carry an active wedge snapshot that was read before the live API resolved that episode. Preserve the durable resolution for the same episode so changed-column persistence, task.json projection, and cache publication cannot reactivate an acknowledged operator notification; a genuinely new wedge must use a new episode ID.
|
|
*/
|
|
export function preserveDurableTaskWedgeInvariants(existingRow: Pick<TaskRow, "wedgeNotification">, task: Task): void {
|
|
const durable = fromJson<Task["wedgeNotification"]>(existingRow.wedgeNotification);
|
|
const incoming = task.wedgeNotification;
|
|
// Keep the legacy resolved-episode rule first and byte-for-byte equivalent in behavior.
|
|
if (
|
|
durable?.status === "resolved"
|
|
&& incoming?.status === "active"
|
|
&& durable.episodeId === incoming.episodeId
|
|
) {
|
|
task.wedgeNotification = durable;
|
|
return;
|
|
}
|
|
/*
|
|
FNXC:TaskWedgeNotifications 2026-08-10-18:54:
|
|
Wedge JSON is persisted wholesale from task snapshots. A newer durable budget revision
|
|
must win over an ordinary stale writer, including durable absence after a reset, without
|
|
overwriting the caller's unrelated episode fields.
|
|
*/
|
|
const durableRevision = durable?.budgetRevision ?? 0;
|
|
const incomingRevision = incoming?.budgetRevision ?? 0;
|
|
if (durableRevision > incomingRevision) {
|
|
task.wedgeNotification = {
|
|
...(incoming ?? durable ?? {} as Task["wedgeNotification"]),
|
|
...(durable ? { budgetRevision: durableRevision, autoRecovery: durable.autoRecovery } : {}),
|
|
} as Task["wedgeNotification"];
|
|
}
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskLifecyclePersistence 2026-07-14-13:27:
|
|
PostgreSQL task JSONB conversion must use one registry for both descriptor writes and SQLite-shaped row hydration. Separate read/write lists drifted when late lifecycle columns were added, allowing JSON strings or parsed objects to cross the wrong serialization boundary.
|
|
*/
|
|
export const TASK_JSONB_COLUMNS: ReadonlySet<string> = new Set([
|
|
"dependencies", "steps", "customFields", "log", "attachments", "steeringComments",
|
|
"comments", "review", "reviewState", "workflowStepResults", "prInfo", "prInfos",
|
|
"issueInfo", "githubTracking", "gitlabTracking", "mergeDetails", "workspaceWorktrees", "repositoryScope", "enabledWorkflowSteps",
|
|
"modifiedFiles", "declaredSymbols", "scopeAutoWiden", "sourceMetadata", "tokenUsagePerModel",
|
|
"tokenBudgetOverride", "columnDwellMs", "workflowTransitionNotification", "recommendations",
|
|
]);
|
|
|
|
export function defineTaskColumn(
|
|
column: keyof TaskRow,
|
|
serialize: TaskColumnDescriptor["serialize"],
|
|
sqlIdentifier: string = column,
|
|
): TaskColumnDescriptor {
|
|
return { column, sqlIdentifier, serialize };
|
|
}
|
|
|
|
const serializeTaskAutoMerge: TaskColumnDescriptor["serialize"] = (task) => task.autoMerge === undefined ? null : (task.autoMerge ? 1 : 0);
|
|
const serializeTaskAutoMergeProvenance: TaskColumnDescriptor["serialize"] = (task) => task.autoMergeProvenance ?? null;
|
|
// FNXC:PlannerOversight 2026-07-14-18:11: three-state like autoMerge — null inherits project default.
|
|
const serializeTaskSessionAdvisorEnabled: TaskColumnDescriptor["serialize"] = (task) =>
|
|
task.sessionAdvisorEnabled === undefined ? null : (task.sessionAdvisorEnabled ? 1 : 0);
|
|
|
|
// Keep this descriptor order in lockstep with the named-column INSERT/UPSERT
|
|
// clauses we generate below. SQLite binds by the explicit column list we emit,
|
|
// so this logical persist order does not need to match the table's physical
|
|
// column layout from CREATE TABLE + migrations.
|
|
export const TASK_COLUMN_DESCRIPTORS: TaskColumnDescriptor[] = [
|
|
defineTaskColumn("id", (task) => task.id),
|
|
defineTaskColumn("lineageId", (_task, context) => context.lineageId),
|
|
defineTaskColumn("title", (task) => task.title ?? null),
|
|
defineTaskColumn("description", (task) => task.description ?? ""),
|
|
defineTaskColumn("priority", (task) => normalizeTaskPriority(task.priority)),
|
|
defineTaskColumn("column", (task) => task.column, '"column"'),
|
|
defineTaskColumn("status", (task) => task.status ?? null),
|
|
defineTaskColumn("size", (task) => task.size ?? null),
|
|
defineTaskColumn("reviewLevel", (task) => task.reviewLevel ?? null),
|
|
defineTaskColumn("currentStep", (task) => task.currentStep || 0),
|
|
defineTaskColumn("worktree", (task) => task.worktree ?? null),
|
|
defineTaskColumn("blockedBy", (task) => task.blockedBy ?? null),
|
|
defineTaskColumn("overlapBlockedBy", (task) => task.overlapBlockedBy ?? null),
|
|
defineTaskColumn("queuedLogEpisodeSignature", (task) => task.queuedLogEpisodeSignature ?? null),
|
|
defineTaskColumn("paused", (task) => task.paused ? 1 : 0),
|
|
defineTaskColumn("pausedReason", (task) => task.pausedReason ?? null),
|
|
defineTaskColumn("wedgeNotification", (task) => toJsonNullable(task.wedgeNotification)),
|
|
defineTaskColumn("userPaused", (task) => task.userPaused ? 1 : 0),
|
|
defineTaskColumn("baseBranch", (task) => task.baseBranch ?? null),
|
|
defineTaskColumn("branch", (task) => task.branch ?? null),
|
|
defineTaskColumn("autoMerge", serializeTaskAutoMerge),
|
|
defineTaskColumn("autoMergeProvenance", serializeTaskAutoMergeProvenance),
|
|
defineTaskColumn("executionStartBranch", (task) => task.executionStartBranch ?? null),
|
|
defineTaskColumn("baseCommitSha", (task) => task.baseCommitSha ?? null),
|
|
defineTaskColumn("modelPresetId", (task) => task.modelPresetId ?? null),
|
|
defineTaskColumn("modelProvider", (task) => task.modelProvider ?? null),
|
|
defineTaskColumn("credentialInstanceId", (task) => task.credentialInstanceId ?? null),
|
|
defineTaskColumn("modelId", (task) => task.modelId ?? null),
|
|
defineTaskColumn("validatorModelProvider", (task) => task.validatorModelProvider ?? null),
|
|
defineTaskColumn("validatorCredentialInstanceId", (task) => task.validatorCredentialInstanceId ?? null),
|
|
defineTaskColumn("validatorModelId", (task) => task.validatorModelId ?? null),
|
|
defineTaskColumn("planningModelProvider", (task) => task.planningModelProvider ?? null),
|
|
defineTaskColumn("planningCredentialInstanceId", (task) => task.planningCredentialInstanceId ?? null),
|
|
defineTaskColumn("planningModelId", (task) => task.planningModelId ?? null),
|
|
defineTaskColumn("mergerModelProvider", (task) => task.mergerModelProvider ?? null),
|
|
defineTaskColumn("mergerCredentialInstanceId", (task) => task.mergerCredentialInstanceId ?? null),
|
|
defineTaskColumn("mergerModelId", (task) => task.mergerModelId ?? null),
|
|
defineTaskColumn("mergeRetries", (task) => task.mergeRetries ?? null),
|
|
defineTaskColumn("aiMergeReviewReconciliation", (task) => toJsonNullable(task.aiMergeReviewReconciliation)),
|
|
defineTaskColumn("workflowStepRetries", (task) => task.workflowStepRetries ?? null),
|
|
defineTaskColumn("stuckKillCount", (task) => task.stuckKillCount ?? 0),
|
|
defineTaskColumn("resumeLimboCount", (task) => task.resumeLimboCount ?? 0),
|
|
defineTaskColumn("graphResumeRetryCount", (task) => task.graphResumeRetryCount === undefined ? 0 : task.graphResumeRetryCount),
|
|
defineTaskColumn("consecutiveToolFailureRetryCount", (task) => task.consecutiveToolFailureRetryCount ?? 0),
|
|
defineTaskColumn("executorEscalationAttempted", (task) => task.executorEscalationAttempted ? 1 : 0),
|
|
defineTaskColumn("toolFailureDetectorLogCursor", (task) => task.toolFailureDetectorLogCursor ?? null),
|
|
// FNXC:ExecutorToolFailureRetry 2026-07-16-13:30: PostgreSQL stores this CAS marker as integer (0/1), matching its schema and legacy SQLite flag representation.
|
|
defineTaskColumn("toolFailureRetryExhaustedAuditEmitted", (task) => task.toolFailureRetryExhaustedAuditEmitted ? 1 : 0),
|
|
defineTaskColumn("resumeLimboTipSha", (task) => task.resumeLimboTipSha ?? null),
|
|
defineTaskColumn("resumeLimboStepSignature", (task) => task.resumeLimboStepSignature ?? null),
|
|
// FNXC:WorkflowLifecycle 2026-07-12 (merge port from main): FN-7863 progress-anchored execute self-requeue streak.
|
|
defineTaskColumn("executeRequeueLoopCount", (task) => task.executeRequeueLoopCount ?? 0),
|
|
defineTaskColumn("executeRequeueLoopSignature", (task) => task.executeRequeueLoopSignature ?? null),
|
|
defineTaskColumn("postReviewFixCount", (task) => task.postReviewFixCount ?? 0),
|
|
defineTaskColumn("planReviewReplanCount", (task) => task.planReviewReplanCount ?? 0),
|
|
defineTaskColumn("recoveryRetryCount", (task) => task.recoveryRetryCount ?? null),
|
|
defineTaskColumn("taskDoneRetryCount", (task) => task.taskDoneRetryCount ?? 0),
|
|
// FNXC:Lifecycle 2026-07-16-21:40: FN-8141 skip-bypass taint marker persisted as nullable ISO timestamp.
|
|
defineTaskColumn("bulkCompletionRefusalAt", (task) => task.bulkCompletionRefusalAt ?? null),
|
|
// FNXC:WorkflowIrPin 2026-07-19-03:10: U9b/KTD-3 — the pin must round-trip through persist so
|
|
// it survives the crash it exists to defend against.
|
|
defineTaskColumn("workflowIrPin", (task) => task.workflowIrPin ?? null),
|
|
defineTaskColumn("workflowIrPinNodeId", (task) => task.workflowIrPinNodeId ?? null),
|
|
defineTaskColumn("workflowIrPinColumnId", (task) => task.workflowIrPinColumnId ?? null),
|
|
// FNXC:LegacyAdoption 2026-07-19-03:10: U9b/KTD-8 one-time adoption stamp.
|
|
defineTaskColumn("legacyAdoptedAt", (task) => task.legacyAdoptedAt ?? null),
|
|
defineTaskColumn("worktreeSessionRetryCount", (task) => task.worktreeSessionRetryCount ?? 0),
|
|
defineTaskColumn("completionHandoffLimboRecoveryCount", (task) => task.completionHandoffLimboRecoveryCount ?? 0),
|
|
defineTaskColumn("verificationFailureCount", (task) => task.verificationFailureCount ?? 0),
|
|
defineTaskColumn("mergeConflictBounceCount", (task) => task.mergeConflictBounceCount ?? 0),
|
|
defineTaskColumn("mergeAuditBounceCount", (task) => task.mergeAuditBounceCount ?? 0),
|
|
defineTaskColumn("mergeTransientRetryCount", (task) => task.mergeTransientRetryCount ?? 0),
|
|
defineTaskColumn("branchConflictRecoveryCount", (task) => task.branchConflictRecoveryCount ?? 0),
|
|
defineTaskColumn("reviewerContextRetryCount", (task) => task.reviewerContextRetryCount ?? 0),
|
|
defineTaskColumn("reviewerFallbackRetryCount", (task) => task.reviewerFallbackRetryCount ?? 0),
|
|
defineTaskColumn("nextRecoveryAt", (task) => task.nextRecoveryAt ?? null),
|
|
defineTaskColumn("error", (task) => task.error ?? null),
|
|
defineTaskColumn("summary", (task) => task.summary ?? null),
|
|
defineTaskColumn("recommendations", (task) => toJsonNullable(task.recommendations)),
|
|
defineTaskColumn("thinkingLevel", (task) => task.thinkingLevel ?? null),
|
|
// FNXC:Settings-ThinkingLevel 2026-07-13 (merge port): per-task validator/planning reasoning-effort overrides.
|
|
defineTaskColumn("validatorThinkingLevel", (task) => task.validatorThinkingLevel ?? null),
|
|
defineTaskColumn("planningThinkingLevel", (task) => task.planningThinkingLevel ?? null),
|
|
defineTaskColumn("mergerThinkingLevel", (task) => task.mergerThinkingLevel ?? null),
|
|
defineTaskColumn("executionMode", (task) => task.executionMode ?? null),
|
|
defineTaskColumn("sessionAdvisorEnabled", serializeTaskSessionAdvisorEnabled),
|
|
defineTaskColumn("tokenUsageInputTokens", (task) => task.tokenUsage?.inputTokens ?? null),
|
|
defineTaskColumn("tokenUsageOutputTokens", (task) => task.tokenUsage?.outputTokens ?? null),
|
|
defineTaskColumn("tokenUsageCachedTokens", (task) => task.tokenUsage?.cachedTokens ?? null),
|
|
defineTaskColumn("tokenUsageCacheWriteTokens", (task) => task.tokenUsage?.cacheWriteTokens ?? null),
|
|
defineTaskColumn("tokenUsageTotalTokens", (task) => task.tokenUsage?.totalTokens ?? null),
|
|
defineTaskColumn("tokenUsageFirstUsedAt", (task) => task.tokenUsage?.firstUsedAt ?? null),
|
|
defineTaskColumn("tokenUsageLastUsedAt", (task) => task.tokenUsage?.lastUsedAt ?? null),
|
|
defineTaskColumn("tokenUsageModelProvider", (task) => task.tokenUsage?.modelProvider ?? null),
|
|
defineTaskColumn("tokenUsageModelId", (task) => task.tokenUsage?.modelId ?? null),
|
|
defineTaskColumn("tokenUsagePerModel", (task) => toJsonNullable(task.tokenUsage?.perModel)),
|
|
defineTaskColumn("tokenBudgetSoftAlertedAt", (task) => task.tokenBudgetSoftAlertedAt ?? null),
|
|
defineTaskColumn("tokenBudgetHardAlertedAt", (task) => task.tokenBudgetHardAlertedAt ?? null),
|
|
defineTaskColumn("tokenBudgetOverride", (task) => toJsonNullable(task.tokenBudgetOverride)),
|
|
defineTaskColumn("createdAt", (task) => task.createdAt),
|
|
defineTaskColumn("updatedAt", (task) => task.updatedAt),
|
|
defineTaskColumn("columnMovedAt", (task) => task.columnMovedAt ?? null),
|
|
defineTaskColumn("firstExecutionAt", (task) => task.firstExecutionAt ?? null),
|
|
defineTaskColumn("cumulativeActiveMs", (task) => task.cumulativeActiveMs ?? null),
|
|
defineTaskColumn("cumulativePlanningMs", (task) => task.cumulativePlanningMs ?? null),
|
|
defineTaskColumn("planningStartedAt", (task) => task.planningStartedAt ?? null),
|
|
/*
|
|
FNXC:TaskLifecyclePersistence 2026-07-14-13:17:
|
|
Persist the late task lifecycle fields through the shared descriptor seam so both SQLite and PostgreSQL retain per-column timing, workflow transition dedupe, oversight overrides, and manual-plan approval state after migration.
|
|
*/
|
|
defineTaskColumn("columnDwellMs", (task) => toJsonNullable(task.columnDwellMs)),
|
|
defineTaskColumn("workflowTransitionNotification", (task) => toJsonNullable(task.workflowTransitionNotification)),
|
|
defineTaskColumn("plannerOversightLevel", (task) => task.plannerOversightLevel ?? null),
|
|
defineTaskColumn("awaitingApprovalReason", (task) => task.awaitingApprovalReason ?? null),
|
|
defineTaskColumn("approvedPlanFingerprint", (task) => task.approvedPlanFingerprint ?? null),
|
|
defineTaskColumn("executionStartedAt", (task) => task.executionStartedAt ?? null),
|
|
defineTaskColumn("executionCompletedAt", (task) => task.executionCompletedAt ?? null),
|
|
defineTaskColumn("dependencies", (task) => toJson(task.dependencies || [])),
|
|
defineTaskColumn("steps", (task) => toJson(task.steps || [])),
|
|
defineTaskColumn("customFields", (task) => toJson(task.customFields ?? {})),
|
|
defineTaskColumn("log", (task) => toJson(task.log || [])),
|
|
defineTaskColumn("attachments", (task) => toJson(task.attachments || [])),
|
|
defineTaskColumn("steeringComments", (task) => toJson(task.steeringComments || [])),
|
|
defineTaskColumn("comments", (task) => toJson(task.comments || [])),
|
|
defineTaskColumn("review", (task) => toJsonNullable(task.review)),
|
|
defineTaskColumn("reviewState", (task) => toJsonNullable(task.reviewState)),
|
|
defineTaskColumn("workflowStepResults", (task) => toJson(task.workflowStepResults || [])),
|
|
defineTaskColumn("prInfo", (task) => toJsonNullable(task.prInfo)),
|
|
defineTaskColumn("prInfos", (task) => toJson(task.prInfos || [])),
|
|
defineTaskColumn("issueInfo", (task) => toJsonNullable(task.issueInfo)),
|
|
defineTaskColumn("githubTracking", (task) => toJsonNullable(task.githubTracking)),
|
|
/*
|
|
FNXC:GitLabTracking 2026-07-16-05:34:
|
|
GitLab import provenance must use the shared task persistence registry like GitHub tracking.
|
|
This closes the partial-feature gap so create and update writes retain metadata for live reads.
|
|
*/
|
|
defineTaskColumn("gitlabTracking", (task) => toJsonNullable(task.gitlabTracking)),
|
|
defineTaskColumn("sourceIssueProvider", (task) => task.sourceIssue?.provider ?? null),
|
|
defineTaskColumn("sourceIssueRepository", (task) => task.sourceIssue?.repository ?? null),
|
|
defineTaskColumn("sourceIssueExternalIssueId", (task) => task.sourceIssue?.externalIssueId ?? null),
|
|
defineTaskColumn("sourceIssueNumber", (task) => task.sourceIssue?.issueNumber ?? null),
|
|
defineTaskColumn("sourceIssueUrl", (task) => task.sourceIssue?.url ?? null),
|
|
defineTaskColumn("sourceIssueClosedAt", (task) => task.sourceIssue?.closedAt ?? null),
|
|
defineTaskColumn("mergeDetails", (task) => toJsonNullable(task.mergeDetails)),
|
|
defineTaskColumn("workspaceWorktrees", (task) => toJsonNullable(task.workspaceWorktrees)),
|
|
defineTaskColumn("repositoryScope", (task) => toJsonNullable(task.repositoryScope)),
|
|
defineTaskColumn("noCommitsExpected", (task) => task.noCommitsExpected ? 1 : 0),
|
|
defineTaskColumn("enabledWorkflowSteps", (task) => toJson(task.enabledWorkflowSteps || [])),
|
|
defineTaskColumn("modifiedFiles", (task) => toJson(task.modifiedFiles || [])),
|
|
defineTaskColumn("declaredSymbols", (task) => toJson(task.declaredSymbols || []), "declared_symbols"),
|
|
defineTaskColumn("missionId", (task) => task.missionId ?? null),
|
|
defineTaskColumn("sliceId", (task) => task.sliceId ?? null),
|
|
defineTaskColumn("scopeOverride", (task) => task.scopeOverride ? 1 : null),
|
|
defineTaskColumn("scopeOverrideReason", (task) => task.scopeOverrideReason ?? null),
|
|
defineTaskColumn("scopeAutoWiden", (task) => toJson(task.scopeAutoWiden || [])),
|
|
defineTaskColumn("assignedAgentId", (task) => task.assignedAgentId ?? null),
|
|
defineTaskColumn("pausedByAgentId", (task) => task.pausedByAgentId ?? null),
|
|
defineTaskColumn("assigneeUserId", (task) => task.assigneeUserId ?? null),
|
|
defineTaskColumn("nodeId", (task) => task.nodeId ?? null),
|
|
defineTaskColumn("effectiveNodeId", (task) => task.effectiveNodeId ?? null),
|
|
defineTaskColumn("effectiveNodeSource", (task) => task.effectiveNodeSource ?? null),
|
|
defineTaskColumn("sourceType", (task) => task.sourceType ?? null),
|
|
defineTaskColumn("sourceAgentId", (task) => task.sourceAgentId ?? null),
|
|
defineTaskColumn("sourceRunId", (task) => task.sourceRunId ?? null),
|
|
defineTaskColumn("sourceSessionId", (task) => task.sourceSessionId ?? null),
|
|
defineTaskColumn("sourceMessageId", (task) => task.sourceMessageId ?? null),
|
|
defineTaskColumn("sourceParentTaskId", (task) => task.sourceParentTaskId ?? null),
|
|
defineTaskColumn("sourceMetadata", (task) => toJsonNullable(task.sourceMetadata)),
|
|
defineTaskColumn("proposalClaimId", (task) => task.proposalClaimId ?? null),
|
|
defineTaskColumn("checkedOutBy", (task) => task.checkedOutBy ?? null),
|
|
defineTaskColumn("checkedOutAt", (task) => task.checkedOutAt ?? null),
|
|
defineTaskColumn("checkoutNodeId", (task) => task.checkoutNodeId ?? null),
|
|
defineTaskColumn("checkoutRunId", (task) => task.checkoutRunId ?? null),
|
|
defineTaskColumn("checkoutLeaseRenewedAt", (task) => task.checkoutLeaseRenewedAt ?? null),
|
|
defineTaskColumn("checkoutLeaseEpoch", (task) => task.checkoutLeaseEpoch ?? 0),
|
|
defineTaskColumn("deletedAt", (task) => task.deletedAt ?? null),
|
|
defineTaskColumn("allowResurrection", (task) => task.allowResurrection ? 1 : 0),
|
|
];
|
|
|
|
export const TASK_COLUMN_DESCRIPTOR_BY_COLUMN = new Map(
|
|
TASK_COLUMN_DESCRIPTORS.map((descriptor) => [descriptor.column, descriptor]),
|
|
);
|
|
export const TASK_PERSIST_SQL_COLUMNS = TASK_COLUMN_DESCRIPTORS.map((descriptor) => descriptor.sqlIdentifier).join(", ");
|
|
export const TASK_UPSERT_SQL_ASSIGNMENTS = TASK_COLUMN_DESCRIPTORS
|
|
.filter((descriptor) => descriptor.column !== "id")
|
|
.map((descriptor) => ` ${descriptor.sqlIdentifier} = excluded.${descriptor.sqlIdentifier}`)
|
|
.join(",\n");
|