FN-8126: add per-task merger model controls
Enable Quick Add and task editing to select merger models and thinking levels. - Persist merger model and thinking overrides through task APIs, storage, and PostgreSQL migrations. - Add merger-lane selection controls to Quick Add and model settings interfaces. - Apply task merger settings to merger and PR fallback sessions, with regression coverage. - Document the merger lane and include a release changeset. Files changed: .changeset/fn-8126-quick-add-merger-lane.md | 7 ++ docs/dashboard-guide.md | 2 + docs/settings-reference.md | 7 +- .../core/src/__tests__/model-resolution.test.ts | 9 ++ packages/core/src/index.gate.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/model-resolution.ts | 20 ++++ .../core/src/postgres/migrations/0000_initial.sql | 3 + .../migrations/0017_task_merger_model_lane.sql | 4 + packages/core/src/postgres/schema-applier.ts | 14 ++- packages/core/src/postgres/schema/project.ts | 3 + packages/core/src/store.ts | 2 +- .../core/src/task-store/archive-lifecycle-2.ts | 6 ++ packages/core/src/task-store/persistence.ts | 6 ++ packages/core/src/task-store/remaining-ops-2.ts | 4 +- packages/core/src/task-store/remaining-ops-6.ts | 2 +- packages/core/src/task-store/serialization.ts | 6 ++ packages/core/src/task-store/task-creation.ts | 6 ++ packages/core/src/task-store/task-row-mappers.ts | 4 +- packages/core/src/task-store/task-update.ts | 6 ++ packages/core/src/types.ts | 18 ++++ packages/dashboard/app/api/tasks.ts | 13 +++ .../dashboard/app/components/InlineCreateCard.tsx | 33 ++++++- .../app/components/ModelSelectionModal.tsx | 29 ++++++ .../dashboard/app/components/ModelSelectorTab.tsx | 101 +++++++++++++++++++-- .../dashboard/app/components/QuickEntryBox.tsx | 44 +++++++-- .../__tests__/ModelSelectionModal.test.tsx | 20 ++++ .../components/__tests__/ModelSelectorTab.test.tsx | 37 +++++++- .../src/routes/register-task-workflow-routes.ts | 27 +++++- .../src/__tests__/agent-session-helpers.test.ts | 8 ++ packages/engine/src/agent-session-helpers.ts | 16 +++- packages/engine/src/merger-ai.ts | 14 +-- packages/engine/src/merger.ts | 35 ++++--- packages/engine/src/pr-response-run-ops.ts | 7 +- 34 files changed, 451 insertions(+), 64 deletions(-) Fusion-Task-Id: FN-8126 Fusion-Task-Lineage: 3fc81801-6d77-4e11-9cf0-3af37313930e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8126-quick-add-merger-lane.md
Normal file
7
.changeset/fn-8126-quick-add-merger-lane.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add a per-task Merger model and thinking selection to the Quick Add model dropdown.
|
||||
category: feature
|
||||
dev: Per-task merger model and thinking overrides are persisted and honored by merger sessions.
|
||||
@@ -507,6 +507,8 @@ Optional workflow steps declared by the active workflow are available from the q
|
||||
<!-- FNXC:QuickAddAttachments 2026-07-16-00:00: Quick Add attachments use a compact icon-only paperclip while keeping the Attach action and pending image count in accessible labels. The same pending preview/upload path accepts image selection, paste, and direct drag/drop onto the Quick Add box. Compact pending-image thumbnails are accessible open controls that show the full image in a movable/resizable window, which becomes a full-screen sheet on mobile. -->
|
||||
<!-- FNXC:QuickAddPriorityIndicator 2026-07-10-21:45: Quick Add keeps status controls in the bottom action cluster: GitHub tracking sits beside the paperclip attach button, Priority is icon-only with low/down, normal/flag, high/up, urgent/alert glyphs, and Fast is an icon-only lightning button.
|
||||
FNXC:PriorityColorCoding 2026-07-11-00:00: Priority glyphs share urgency colors across Quick Add, the New Task inline row, and task-card badges: low=info/blue, normal=muted, high=warning/amber, urgent=error/red. -->
|
||||
Quick Add and Inline Create model selection include Plan, Executor, Reviewer, and Merger lanes. Each lane can inherit its default or select a task-specific model; Plan, Reviewer, and Merger also provide independent thinking-level overrides.
|
||||
|
||||
Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show compact pending previews before task creation and upload the images to the created task afterward. Select a pending preview to inspect the full image in a movable, resizable window (a full-screen sheet on mobile); close it with Escape or the close control to return to the preview. The same bottom action row places the GitHub tracking override beside the paperclip; Priority is an icon-only control whose glyph changes by selected level (down arrow for low, flag for normal, up arrow for high, alert for urgent) and is color-coded by urgency (low blue/info, normal muted, high amber/warning, urgent red/error), and Fast is an icon-only lightning control. These icon-only controls keep accessible labels and the same create-payload behavior as the previous text chips.
|
||||
|
||||
Quick entry, inline quick-create, and the full **New Task** dialog all check for similar active tasks before creating. When possible duplicates exist, the warning lists each match by task description (falling back to title, then “No description”) and lets you open an existing task, cancel, or create anyway with the duplicates acknowledged.
|
||||
|
||||
@@ -1094,14 +1094,15 @@ Mission validation sessions use this same validator lane; assigned durable agent
|
||||
|
||||
Dedicated model lane for merger agent sessions (conflict resolution, clean-room merge, stash-conflict recovery, PR-response helpers, and related merge-agent runs). Configurable under **Settings → Global Models** and **Settings → Project Models**. Does not inherit the executor, planner, or reviewer lanes.
|
||||
|
||||
1. Project `mergerProvider` + `mergerModelId`
|
||||
2. Global `mergerGlobalProvider` + `mergerGlobalModelId`
|
||||
1. Complete per-task `mergerModelProvider` + `mergerModelId`
|
||||
2. Project `mergerProvider` + `mergerModelId`
|
||||
3. Global `mergerGlobalProvider` + `mergerGlobalModelId`
|
||||
3. Project `defaultProviderOverride` + `defaultModelIdOverride`
|
||||
4. Global `defaultProvider` + `defaultModelId`
|
||||
5. Assigned durable agent runtime model (`runtimeConfig.model` or `runtimeConfig.modelProvider` + `runtimeConfig.modelId`) when both provider and model ID are set and no merger/default pair is configured
|
||||
6. Automatic provider/model resolution
|
||||
|
||||
Thinking level for merger sessions: project `mergerThinkingLevel` → global `mergerGlobalThinkingLevel` → project `defaultThinkingLevelOverride` → global `defaultThinkingLevel`.
|
||||
Thinking level for merger sessions: per-task `mergerThinkingLevel` → project `mergerThinkingLevel` → global `mergerGlobalThinkingLevel` → project `defaultThinkingLevelOverride` → global `defaultThinkingLevel`.
|
||||
|
||||
Session-level fallback on retryable failures resolves project `mergerFallbackProvider` + `mergerFallbackModelId` first, then the shared global `fallbackProvider` + `fallbackModelId` pair. Partial project fallback pairs are ignored. This lane applies to every merger session, including merger-ai mutating and review agents and the PR-response runner.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskMergerModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveMergerFallbackModel,
|
||||
@@ -16,6 +17,14 @@ import {
|
||||
} from "../model-resolution.js";
|
||||
|
||||
describe("model-resolution", () => {
|
||||
it("uses only a complete task merger pair before settings and preserves test mode", () => {
|
||||
const settings = { mergerProvider: "settings-provider", mergerModelId: "settings-model" };
|
||||
expect(resolveTaskMergerModel({ mergerModelProvider: "task-provider", mergerModelId: "task-model" }, settings)).toEqual({ provider: "task-provider", modelId: "task-model" });
|
||||
expect(resolveTaskMergerModel({ mergerModelProvider: "partial-provider" }, settings)).toEqual({ provider: "settings-provider", modelId: "settings-model" });
|
||||
expect(resolveTaskMergerModel({}, settings)).toEqual({ provider: "settings-provider", modelId: "settings-model" });
|
||||
expect(resolveTaskMergerModel({ mergerModelProvider: "task-provider", mergerModelId: "task-model" }, { ...settings, testMode: true })).toEqual(TEST_MODE_RESOLVED);
|
||||
});
|
||||
|
||||
it("resolves executor fallback before the shared fallback and forces mock in test mode", () => {
|
||||
expect(resolveExecutorFallbackModel({
|
||||
executionFallbackProvider: "executor-provider",
|
||||
|
||||
@@ -1418,6 +1418,7 @@ export {
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskMergerModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
|
||||
@@ -1479,6 +1479,7 @@ export {
|
||||
resolveProjectDefaultModel,
|
||||
resolveSettingsLaneThinkingLevel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskMergerModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
|
||||
@@ -42,6 +42,8 @@ type TaskModelLike = {
|
||||
validatorModelId?: string | null;
|
||||
planningModelProvider?: string | null;
|
||||
planningModelId?: string | null;
|
||||
mergerModelProvider?: string | null;
|
||||
mergerModelId?: string | null;
|
||||
};
|
||||
|
||||
function hasCompleteModelPair(pair: ModelPair): pair is { provider: string; modelId: string } {
|
||||
@@ -331,6 +333,24 @@ export function resolveTaskPlanningModel(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:Settings-MergerModel 2026-07-16-12:00:
|
||||
* A complete task pair wins before the project/global merger lane. Partial pairs
|
||||
* are deliberately ignored, preserving the established lane-pair invariant.
|
||||
*/
|
||||
export function resolveTaskMergerModel(
|
||||
task: TaskModelLike,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{ provider: task.mergerModelProvider, modelId: task.mergerModelId },
|
||||
resolveMergerSettingsModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Fusion Model Router lane wrappers (U17 / KTD9) ─────────────────────────
|
||||
//
|
||||
// These are the **governed** session-start lanes: execution, planning, and
|
||||
|
||||
@@ -68,6 +68,9 @@ CREATE TABLE IF NOT EXISTS project.tasks (
|
||||
validator_model_id text,
|
||||
planning_model_provider text,
|
||||
planning_model_id text,
|
||||
merger_model_provider text,
|
||||
merger_model_id text,
|
||||
merger_thinking_level text,
|
||||
merge_retries integer,
|
||||
workflow_step_retries integer,
|
||||
resume_limbo_count integer DEFAULT 0,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- FNXC:Settings-MergerModel 2026-07-16-12:00: existing project task rows need independent merger pair and thinking overrides.
|
||||
ALTER TABLE project.tasks ADD COLUMN IF NOT EXISTS merger_model_provider text;
|
||||
ALTER TABLE project.tasks ADD COLUMN IF NOT EXISTS merger_model_id text;
|
||||
ALTER TABLE project.tasks ADD COLUMN IF NOT EXISTS merger_thinking_level text;
|
||||
@@ -31,7 +31,7 @@ import { runPluginSchemaInitHooks, DEFAULT_PLUGIN_SCHEMA_INIT_HOOKS, type Plugin
|
||||
FNXC:MultiProjectIsolation 2026-07-15-23:40:
|
||||
Advances to 0012 after the owner_project_id domain/partition split and chat pin timestamp. Per-migration identities above stay fixed; only this latest-version marker moves.
|
||||
*/
|
||||
export const SCHEMA_BASELINE_VERSION = "0016";
|
||||
export const SCHEMA_BASELINE_VERSION = "0017";
|
||||
const INITIAL_SCHEMA_VERSION = "0000";
|
||||
const AUTOMATION_ISOLATION_SCHEMA_VERSION = "0001";
|
||||
const ANALYTICS_ISOLATION_SCHEMA_VERSION = "0002";
|
||||
@@ -84,6 +84,8 @@ export const EXECUTOR_TOOL_FAILURE_RETRY_VERSION = "0013";
|
||||
export const EXECUTOR_ESCALATION_ATTEMPT_VERSION = "0014";
|
||||
/** FNXC:PostgresSchema 2026-07-16-22:00: central global routines follow main's already-landed 0014 migration. */
|
||||
export const GLOBAL_ROUTINES_SCHEMA_VERSION = "0015";
|
||||
/** FNXC:Settings-MergerModel 2026-07-16-12:00: per-task merger lane is an additive upgrade. */
|
||||
export const TASK_MERGER_MODEL_LANE_VERSION = "0017";
|
||||
|
||||
/** Bookkeeping table for the fresh Drizzle migration history. */
|
||||
export const MIGRATION_BOOKKEEPING_TABLE = "fusion_schema_migrations";
|
||||
@@ -170,6 +172,7 @@ const GLOBAL_ROUTINES_MIGRATION_PATH = join(
|
||||
"migrations",
|
||||
"0015_global_routines.sql",
|
||||
);
|
||||
const TASK_MERGER_MODEL_LANE_MIGRATION_PATH = join(__dirname, "migrations", "0017_task_merger_model_lane.sql");
|
||||
|
||||
/**
|
||||
* Ensure the migration bookkeeping table exists. Lives in the public schema so
|
||||
@@ -255,6 +258,7 @@ export async function applySchemaBaseline(
|
||||
const executorToolFailureRetryAlreadyApplied = applied.includes(EXECUTOR_TOOL_FAILURE_RETRY_VERSION);
|
||||
const executorEscalationAttemptAlreadyApplied = applied.includes(EXECUTOR_ESCALATION_ATTEMPT_VERSION);
|
||||
const globalRoutinesAlreadyApplied = applied.includes(GLOBAL_ROUTINES_SCHEMA_VERSION);
|
||||
const taskMergerModelLaneAlreadyApplied = applied.includes(TASK_MERGER_MODEL_LANE_VERSION);
|
||||
let schemaChanged = false;
|
||||
|
||||
if (!baselineAlreadyApplied) {
|
||||
@@ -547,6 +551,14 @@ export async function applySchemaBaseline(
|
||||
);
|
||||
schemaChanged = true;
|
||||
}
|
||||
if (!taskMergerModelLaneAlreadyApplied) {
|
||||
const migrationSql = await readFile(TASK_MERGER_MODEL_LANE_MIGRATION_PATH, "utf8");
|
||||
await tx.execute(sql.raw(migrationSql));
|
||||
await tx.execute(
|
||||
sql`INSERT INTO public.${sql.identifier(MIGRATION_BOOKKEEPING_TABLE)} (version) VALUES (${TASK_MERGER_MODEL_LANE_VERSION}) ON CONFLICT (version) DO NOTHING`,
|
||||
);
|
||||
schemaChanged = true;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitHubImportTranslate 2026-07-16-23:30:
|
||||
|
||||
@@ -96,6 +96,9 @@ export const tasks = projectSchema.table("tasks", {
|
||||
validatorModelId: text("validator_model_id"),
|
||||
planningModelProvider: text("planning_model_provider"),
|
||||
planningModelId: text("planning_model_id"),
|
||||
mergerModelProvider: text("merger_model_provider"),
|
||||
mergerModelId: text("merger_model_id"),
|
||||
mergerThinkingLevel: text("merger_thinking_level"),
|
||||
mergeRetries: integer("merge_retries"),
|
||||
workflowStepRetries: integer("workflow_step_retries"),
|
||||
resumeLimboCount: integer("resume_limbo_count").default(0),
|
||||
|
||||
@@ -1178,7 +1178,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
async updateTask(
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; workspaceWorktrees?: import("./types.js").Task["workspaceWorktrees"]; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; customFields?: Record<string, unknown>; currentStep?: number; blockedBy?: string | null; overlapBlockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; worktrunkFallbackAlertedAt?: string | null; worktrunkFailure?: import("./types.js").Task["worktrunkFailure"] | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("./types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; scopeAutoWiden?: string[] | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; autoMerge?: boolean | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; resumeLimboCount?: number | null; executeRequeueLoopCount?: number | null; graphResumeRetryCount?: number | null; consecutiveToolFailureRetryCount?: number | null; executorEscalationAttempted?: boolean | null; toolFailureDetectorLogCursor?: number | null; toolFailureRetryExhaustedAuditEmitted?: boolean | null; resumeLimboTipSha?: string | null; resumeLimboStepSignature?: string | null; executeRequeueLoopSignature?: string | null; postReviewFixCount?: number | null; planReviewReplanCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; bulkCompletionRefusalAt?: 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; enabledWorkflowSteps?: string[]; noCommitsExpected?: boolean | null; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; validatorThinkingLevel?: string | null; planningThinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; firstExecutionAt?: string | null; cumulativeActiveMs?: number | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; sourceMetadataPatch?: Record<string, unknown> | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null; workflowTransitionNotification?: import("./types.js").WorkflowTransitionNotificationMarker | undefined; plannerOversightLevel?: string | null; sessionAdvisorEnabled?: boolean | null; approvedPlanFingerprint?: string | null }, runContext?: RunMutationContext,
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; workspaceWorktrees?: import("./types.js").Task["workspaceWorktrees"]; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; customFields?: Record<string, unknown>; currentStep?: number; blockedBy?: string | null; overlapBlockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; worktrunkFallbackAlertedAt?: string | null; worktrunkFailure?: import("./types.js").Task["worktrunkFailure"] | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("./types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; scopeAutoWiden?: string[] | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; autoMerge?: boolean | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; resumeLimboCount?: number | null; executeRequeueLoopCount?: number | null; graphResumeRetryCount?: number | null; consecutiveToolFailureRetryCount?: number | null; executorEscalationAttempted?: boolean | null; toolFailureDetectorLogCursor?: number | null; toolFailureRetryExhaustedAuditEmitted?: boolean | null; resumeLimboTipSha?: string | null; resumeLimboStepSignature?: string | null; executeRequeueLoopSignature?: string | null; postReviewFixCount?: number | null; planReviewReplanCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; bulkCompletionRefusalAt?: 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; enabledWorkflowSteps?: string[]; noCommitsExpected?: boolean | null; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; mergerModelProvider?: string | null; mergerModelId?: string | null; thinkingLevel?: string | null; validatorThinkingLevel?: string | null; planningThinkingLevel?: string | null; mergerThinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; firstExecutionAt?: string | null; cumulativeActiveMs?: number | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; sourceMetadataPatch?: Record<string, unknown> | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null; workflowTransitionNotification?: import("./types.js").WorkflowTransitionNotificationMarker | undefined; plannerOversightLevel?: string | null; sessionAdvisorEnabled?: boolean | null; approvedPlanFingerprint?: string | null }, runContext?: RunMutationContext,
|
||||
): Promise<Task> {
|
||||
return updateTaskImpl(this, id, updates, runContext);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ export async function taskToArchiveEntryImpl(store: TaskStore, task: Task, archi
|
||||
validatorModelId: task.validatorModelId,
|
||||
planningModelProvider: task.planningModelProvider,
|
||||
planningModelId: task.planningModelId,
|
||||
mergerModelProvider: task.mergerModelProvider,
|
||||
mergerModelId: task.mergerModelId,
|
||||
mergerThinkingLevel: task.mergerThinkingLevel,
|
||||
breakIntoSubtasks: task.breakIntoSubtasks,
|
||||
noCommitsExpected: task.noCommitsExpected,
|
||||
baseBranch: task.baseBranch,
|
||||
@@ -454,6 +457,9 @@ export async function restoreFromArchiveImpl(store: TaskStore, entry: import("..
|
||||
validatorModelId: entry.validatorModelId,
|
||||
planningModelProvider: entry.planningModelProvider,
|
||||
planningModelId: entry.planningModelId,
|
||||
mergerModelProvider: entry.mergerModelProvider,
|
||||
mergerModelId: entry.mergerModelId,
|
||||
mergerThinkingLevel: entry.mergerThinkingLevel,
|
||||
breakIntoSubtasks: entry.breakIntoSubtasks,
|
||||
noCommitsExpected: entry.noCommitsExpected,
|
||||
modifiedFiles: entry.modifiedFiles,
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface TaskRow {
|
||||
validatorModelId: string | null;
|
||||
planningModelProvider: string | null;
|
||||
planningModelId: string | null;
|
||||
mergerModelProvider: string | null;
|
||||
mergerModelId: string | null;
|
||||
mergeRetries: number | null;
|
||||
workflowStepRetries: number | null;
|
||||
stuckKillCount: number | null;
|
||||
@@ -76,6 +78,7 @@ export interface TaskRow {
|
||||
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;
|
||||
@@ -229,6 +232,8 @@ export const TASK_COLUMN_DESCRIPTORS: TaskColumnDescriptor[] = [
|
||||
defineTaskColumn("validatorModelId", (task) => task.validatorModelId ?? null),
|
||||
defineTaskColumn("planningModelProvider", (task) => task.planningModelProvider ?? null),
|
||||
defineTaskColumn("planningModelId", (task) => task.planningModelId ?? null),
|
||||
defineTaskColumn("mergerModelProvider", (task) => task.mergerModelProvider ?? null),
|
||||
defineTaskColumn("mergerModelId", (task) => task.mergerModelId ?? null),
|
||||
defineTaskColumn("mergeRetries", (task) => task.mergeRetries ?? null),
|
||||
defineTaskColumn("workflowStepRetries", (task) => task.workflowStepRetries ?? null),
|
||||
defineTaskColumn("stuckKillCount", (task) => task.stuckKillCount ?? 0),
|
||||
@@ -266,6 +271,7 @@ export const TASK_COLUMN_DESCRIPTORS: TaskColumnDescriptor[] = [
|
||||
// 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),
|
||||
|
||||
@@ -43,9 +43,9 @@ export function getTaskSelectClauseWithActivityLogLimitImpl(store: TaskStore, li
|
||||
"worktree", "blockedBy", "overlapBlockedBy", "paused", "pausedReason", "userPaused", "baseBranch", "branch", "autoMerge", "autoMergeProvenance", "executionStartBranch", "baseCommitSha",
|
||||
"modelPresetId", "modelProvider", "modelId",
|
||||
"validatorModelProvider", "validatorModelId",
|
||||
"planningModelProvider", "planningModelId",
|
||||
"planningModelProvider", "planningModelId", "mergerModelProvider", "mergerModelId",
|
||||
"mergeRetries", "workflowStepRetries", "stuckKillCount", "resumeLimboCount", "executeRequeueLoopCount", "graphResumeRetryCount", "consecutiveToolFailureRetryCount", "executorEscalationAttempted", "toolFailureDetectorLogCursor", "toolFailureRetryExhaustedAuditEmitted", "resumeLimboTipSha", "resumeLimboStepSignature", "executeRequeueLoopSignature", "postReviewFixCount", "planReviewReplanCount", "recoveryRetryCount", "taskDoneRetryCount", "bulkCompletionRefusalAt", "worktreeSessionRetryCount", "completionHandoffLimboRecoveryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "mergeTransientRetryCount", "branchConflictRecoveryCount", "reviewerContextRetryCount", "reviewerFallbackRetryCount", "nextRecoveryAt",
|
||||
"error", "summary", "thinkingLevel", "validatorThinkingLevel", "planningThinkingLevel", "executionMode",
|
||||
"error", "summary", "thinkingLevel", "validatorThinkingLevel", "planningThinkingLevel", "mergerThinkingLevel", "executionMode",
|
||||
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenUsageModelProvider", "tokenUsageModelId", "tokenUsagePerModel", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride",
|
||||
"createdAt", "updatedAt", "columnMovedAt", "firstExecutionAt", "cumulativeActiveMs", "executionStartedAt", "executionCompletedAt",
|
||||
"dependencies", "steps", "customFields", "attachments", "steeringComments",
|
||||
|
||||
@@ -731,7 +731,7 @@ export async function resetPromptCheckboxesImpl(store: TaskStore, dir: string):
|
||||
|
||||
export async function updateTaskImpl(store: TaskStore,
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; workspaceWorktrees?: import("../types.js").Task["workspaceWorktrees"]; status?: string | null; dependencies?: string[]; steps?: import("../types.js").TaskStep[]; customFields?: Record<string, unknown>; currentStep?: number; blockedBy?: string | null; overlapBlockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; worktrunkFallbackAlertedAt?: string | null; worktrunkFailure?: import("../types.js").Task["worktrunkFailure"] | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("../types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; scopeAutoWiden?: string[] | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; autoMerge?: boolean | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("../types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; resumeLimboCount?: number | null; executeRequeueLoopCount?: number | null; graphResumeRetryCount?: number | null; consecutiveToolFailureRetryCount?: number | null; executorEscalationAttempted?: boolean | null; toolFailureDetectorLogCursor?: number | null; toolFailureRetryExhaustedAuditEmitted?: boolean | null; resumeLimboTipSha?: string | null; resumeLimboStepSignature?: string | null; executeRequeueLoopSignature?: string | null; postReviewFixCount?: number | null; planReviewReplanCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; bulkCompletionRefusalAt?: 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; enabledWorkflowSteps?: string[]; noCommitsExpected?: boolean | null; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; validatorThinkingLevel?: string | null; planningThinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; firstExecutionAt?: string | null; cumulativeActiveMs?: number | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("../types.js").TaskReview | null; reviewState?: import("../types.js").TaskReviewState | null; workflowStepResults?: import("../types.js").WorkflowStepResult[] | null; mergeDetails?: import("../types.js").MergeDetails | null; sourceIssue?: import("../types.js").TaskSourceIssue | null; sourceMetadataPatch?: Record<string, unknown> | null; githubTracking?: import("../types.js").TaskGithubTracking | null; tokenUsage?: import("../types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null; workflowTransitionNotification?: import("../types.js").WorkflowTransitionNotificationMarker | undefined; sessionAdvisorEnabled?: boolean | null }, runContext?: RunMutationContext,
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; workspaceWorktrees?: import("../types.js").Task["workspaceWorktrees"]; status?: string | null; dependencies?: string[]; steps?: import("../types.js").TaskStep[]; customFields?: Record<string, unknown>; currentStep?: number; blockedBy?: string | null; overlapBlockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; worktrunkFallbackAlertedAt?: string | null; worktrunkFailure?: import("../types.js").Task["worktrunkFailure"] | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("../types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; scopeAutoWiden?: string[] | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; autoMerge?: boolean | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("../types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; resumeLimboCount?: number | null; executeRequeueLoopCount?: number | null; graphResumeRetryCount?: number | null; consecutiveToolFailureRetryCount?: number | null; executorEscalationAttempted?: boolean | null; toolFailureDetectorLogCursor?: number | null; toolFailureRetryExhaustedAuditEmitted?: boolean | null; resumeLimboTipSha?: string | null; resumeLimboStepSignature?: string | null; executeRequeueLoopSignature?: string | null; postReviewFixCount?: number | null; planReviewReplanCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; bulkCompletionRefusalAt?: 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; enabledWorkflowSteps?: string[]; noCommitsExpected?: boolean | null; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; mergerModelProvider?: string | null; mergerModelId?: string | null; thinkingLevel?: string | null; validatorThinkingLevel?: string | null; planningThinkingLevel?: string | null; mergerThinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; firstExecutionAt?: string | null; cumulativeActiveMs?: number | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("../types.js").TaskReview | null; reviewState?: import("../types.js").TaskReviewState | null; workflowStepResults?: import("../types.js").WorkflowStepResult[] | null; mergeDetails?: import("../types.js").MergeDetails | null; sourceIssue?: import("../types.js").TaskSourceIssue | null; sourceMetadataPatch?: Record<string, unknown> | null; githubTracking?: import("../types.js").TaskGithubTracking | null; tokenUsage?: import("../types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null; workflowTransitionNotification?: import("../types.js").WorkflowTransitionNotificationMarker | undefined; sessionAdvisorEnabled?: boolean | null }, runContext?: RunMutationContext,
|
||||
): Promise<Task> {
|
||||
/*
|
||||
FNXC:StateMachine 2026-07-07-12:00:
|
||||
|
||||
@@ -96,6 +96,8 @@ export function rowToTask(row: TaskRow): Task {
|
||||
validatorModelId: row.validatorModelId || undefined,
|
||||
planningModelProvider: row.planningModelProvider || undefined,
|
||||
planningModelId: row.planningModelId || undefined,
|
||||
mergerModelProvider: row.mergerModelProvider || undefined,
|
||||
mergerModelId: row.mergerModelId || undefined,
|
||||
mergeRetries: row.mergeRetries ?? undefined,
|
||||
workflowStepRetries: row.workflowStepRetries ?? undefined,
|
||||
stuckKillCount: row.stuckKillCount ?? undefined,
|
||||
@@ -130,6 +132,7 @@ export function rowToTask(row: TaskRow): Task {
|
||||
thinkingLevel: (row.thinkingLevel || undefined) as Task["thinkingLevel"],
|
||||
validatorThinkingLevel: (row.validatorThinkingLevel || undefined) as Task["validatorThinkingLevel"],
|
||||
planningThinkingLevel: (row.planningThinkingLevel || undefined) as Task["planningThinkingLevel"],
|
||||
mergerThinkingLevel: (row.mergerThinkingLevel || undefined) as Task["mergerThinkingLevel"],
|
||||
executionMode: (row.executionMode || undefined) as Task["executionMode"],
|
||||
// FNXC:PlannerOversight 2026-07-14-18:11: null → undefined (inherit); 0/1 → boolean.
|
||||
sessionAdvisorEnabled: row.sessionAdvisorEnabled === null || row.sessionAdvisorEnabled === undefined
|
||||
@@ -360,6 +363,9 @@ export function archiveEntryToTask(
|
||||
validatorModelId: entry.validatorModelId,
|
||||
planningModelProvider: entry.planningModelProvider,
|
||||
planningModelId: entry.planningModelId,
|
||||
mergerModelProvider: entry.mergerModelProvider,
|
||||
mergerModelId: entry.mergerModelId,
|
||||
mergerThinkingLevel: entry.mergerThinkingLevel,
|
||||
breakIntoSubtasks: entry.breakIntoSubtasks,
|
||||
noCommitsExpected: entry.noCommitsExpected,
|
||||
branchContext: entry.branchContext,
|
||||
|
||||
@@ -316,9 +316,12 @@ export async function _createTaskInternalBackendImpl(store: TaskStore, input: Ta
|
||||
validatorModelId: input.validatorModelId,
|
||||
planningModelProvider: input.planningModelProvider,
|
||||
planningModelId: input.planningModelId,
|
||||
mergerModelProvider: input.mergerModelProvider,
|
||||
mergerModelId: input.mergerModelId,
|
||||
thinkingLevel: input.thinkingLevel,
|
||||
validatorThinkingLevel: input.validatorThinkingLevel,
|
||||
planningThinkingLevel: input.planningThinkingLevel,
|
||||
mergerThinkingLevel: input.mergerThinkingLevel,
|
||||
reviewLevel: input.reviewLevel,
|
||||
executionMode: input.executionMode,
|
||||
// FNXC:PlannerOversight 2026-07-14-18:11: only set when create input is explicit boolean.
|
||||
@@ -860,9 +863,12 @@ export async function _createTaskInternalImpl(store: TaskStore, input: TaskCreat
|
||||
validatorModelId: input.validatorModelId,
|
||||
planningModelProvider: input.planningModelProvider,
|
||||
planningModelId: input.planningModelId,
|
||||
mergerModelProvider: input.mergerModelProvider,
|
||||
mergerModelId: input.mergerModelId,
|
||||
thinkingLevel: input.thinkingLevel,
|
||||
validatorThinkingLevel: input.validatorThinkingLevel,
|
||||
planningThinkingLevel: input.planningThinkingLevel,
|
||||
mergerThinkingLevel: input.mergerThinkingLevel,
|
||||
reviewLevel: input.reviewLevel,
|
||||
executionMode: input.executionMode,
|
||||
// FNXC:PlannerOversight 2026-07-14-18:11: only set when create input is explicit boolean.
|
||||
|
||||
@@ -36,9 +36,9 @@ export function getTaskSelectClauseImpl2(store: TaskStore, slim: boolean, tableA
|
||||
"worktree", "blockedBy", "overlapBlockedBy", "paused", "pausedReason", "userPaused", "baseBranch", "branch", "autoMerge", "autoMergeProvenance", "executionStartBranch", "baseCommitSha",
|
||||
"modelPresetId", "modelProvider", "modelId",
|
||||
"validatorModelProvider", "validatorModelId",
|
||||
"planningModelProvider", "planningModelId",
|
||||
"planningModelProvider", "planningModelId", "mergerModelProvider", "mergerModelId",
|
||||
"mergeRetries", "workflowStepRetries", "stuckKillCount", "resumeLimboCount", "executeRequeueLoopCount", "graphResumeRetryCount", "consecutiveToolFailureRetryCount", "executorEscalationAttempted", "toolFailureDetectorLogCursor", "toolFailureRetryExhaustedAuditEmitted", "resumeLimboTipSha", "resumeLimboStepSignature", "executeRequeueLoopSignature", "postReviewFixCount", "planReviewReplanCount", "recoveryRetryCount", "taskDoneRetryCount", "bulkCompletionRefusalAt", "worktreeSessionRetryCount", "completionHandoffLimboRecoveryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "mergeTransientRetryCount", "branchConflictRecoveryCount", "reviewerContextRetryCount", "reviewerFallbackRetryCount", "nextRecoveryAt",
|
||||
"error", "summary", "thinkingLevel", "validatorThinkingLevel", "planningThinkingLevel", "executionMode",
|
||||
"error", "summary", "thinkingLevel", "validatorThinkingLevel", "planningThinkingLevel", "mergerThinkingLevel", "executionMode",
|
||||
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenUsageModelProvider", "tokenUsageModelId", "tokenUsagePerModel", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride",
|
||||
"createdAt", "updatedAt", "columnMovedAt", "firstExecutionAt", "cumulativeActiveMs", "executionStartedAt", "executionCompletedAt",
|
||||
"dependencies", "steps", "customFields", "comments", "review", "reviewState", "workflowStepResults", "steeringComments",
|
||||
|
||||
@@ -514,6 +514,10 @@ export async function updateTaskUnlockedImpl(store: TaskStore, id: string, updat
|
||||
} else if (updates.planningModelId !== undefined) {
|
||||
task.planningModelId = updates.planningModelId;
|
||||
}
|
||||
if (updates.mergerModelProvider === null) task.mergerModelProvider = undefined;
|
||||
else if (updates.mergerModelProvider !== undefined) task.mergerModelProvider = updates.mergerModelProvider;
|
||||
if (updates.mergerModelId === null) task.mergerModelId = undefined;
|
||||
else if (updates.mergerModelId !== undefined) task.mergerModelId = updates.mergerModelId;
|
||||
if (updates.validatorThinkingLevel === null) {
|
||||
task.validatorThinkingLevel = undefined;
|
||||
} else if (updates.validatorThinkingLevel !== undefined) {
|
||||
@@ -524,6 +528,8 @@ export async function updateTaskUnlockedImpl(store: TaskStore, id: string, updat
|
||||
} else if (updates.planningThinkingLevel !== undefined) {
|
||||
task.planningThinkingLevel = updates.planningThinkingLevel as import("../types.js").ThinkingLevel;
|
||||
}
|
||||
if (updates.mergerThinkingLevel === null) task.mergerThinkingLevel = undefined;
|
||||
else if (updates.mergerThinkingLevel !== undefined) task.mergerThinkingLevel = updates.mergerThinkingLevel as import("../types.js").ThinkingLevel;
|
||||
if (updates.thinkingLevel === null) {
|
||||
task.thinkingLevel = undefined;
|
||||
} else if (updates.thinkingLevel !== undefined) {
|
||||
|
||||
@@ -1616,6 +1616,13 @@ export interface Task {
|
||||
* Must be set together with `planningModelProvider`. When both planning model
|
||||
* fields are undefined, the triage agent uses global settings defaults. */
|
||||
planningModelId?: string;
|
||||
/**
|
||||
* FNXC:Settings-MergerModel 2026-07-16-12:00:
|
||||
* Per-task merger overrides take precedence over the project/global merger lane only when both fields are set; merger sessions otherwise retain their existing settings-based resolution.
|
||||
*/
|
||||
mergerModelProvider?: string;
|
||||
/** Must be set together with `mergerModelProvider`. */
|
||||
mergerModelId?: string;
|
||||
/** IDs of workflow steps enabled for this task, run after implementation completes */
|
||||
enabledWorkflowSteps?: string[];
|
||||
/** Results from workflow step executions (populated after task implementation) */
|
||||
@@ -1807,6 +1814,8 @@ export interface Task {
|
||||
*/
|
||||
validatorThinkingLevel?: ThinkingLevel;
|
||||
planningThinkingLevel?: ThinkingLevel;
|
||||
/** Independent per-task merger reasoning-effort override; unset inherits merger settings. */
|
||||
mergerThinkingLevel?: ThinkingLevel;
|
||||
/** Execution mode for task implementation.
|
||||
* - "standard": Full execution with complete review workflow (default)
|
||||
* - "fast": Expedited execution with minimal overhead for simple tasks
|
||||
@@ -2084,6 +2093,9 @@ export interface TaskCreateInput {
|
||||
* Must be set together with `planningModelProvider`. When both planning model
|
||||
* fields are undefined, the triage agent uses global settings defaults. */
|
||||
planningModelId?: string;
|
||||
/** Per-task merger override; provider and model id must be supplied together. */
|
||||
mergerModelProvider?: string;
|
||||
mergerModelId?: string;
|
||||
/** Thinking level for AI agent sessions — controls reasoning effort (off/minimal/low/medium/high) */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
/**
|
||||
@@ -2092,6 +2104,8 @@ export interface TaskCreateInput {
|
||||
*/
|
||||
validatorThinkingLevel?: ThinkingLevel;
|
||||
planningThinkingLevel?: ThinkingLevel;
|
||||
/** Independent per-task merger reasoning-effort override; unset inherits merger settings. */
|
||||
mergerThinkingLevel?: ThinkingLevel;
|
||||
/** When true, trigger AI title summarization if description is long and no title provided */
|
||||
summarize?: boolean;
|
||||
/** Mission ID to link this task to (for mission hierarchy) */
|
||||
@@ -4629,6 +4643,9 @@ export interface ArchivedTaskEntry {
|
||||
/** Optional: planning model override for triage agent */
|
||||
planningModelProvider?: string;
|
||||
planningModelId?: string;
|
||||
mergerModelProvider?: string;
|
||||
mergerModelId?: string;
|
||||
mergerThinkingLevel?: ThinkingLevel;
|
||||
/** Per-task token/cost accounting (input/output/cache) preserved across archival. */
|
||||
tokenUsage?: TaskTokenUsage;
|
||||
/** Optional: other metadata to preserve */
|
||||
@@ -7369,6 +7386,7 @@ export {
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskMergerModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
|
||||
@@ -210,7 +210,12 @@ export async function createTask(
|
||||
validatorModelId,
|
||||
planningModelProvider,
|
||||
planningModelId,
|
||||
mergerModelProvider,
|
||||
mergerModelId,
|
||||
thinkingLevel,
|
||||
validatorThinkingLevel,
|
||||
planningThinkingLevel,
|
||||
mergerThinkingLevel,
|
||||
plannerOversightLevel,
|
||||
summarize,
|
||||
reviewLevel,
|
||||
@@ -249,7 +254,12 @@ export async function createTask(
|
||||
validatorModelId,
|
||||
planningModelProvider,
|
||||
planningModelId,
|
||||
mergerModelProvider,
|
||||
mergerModelId,
|
||||
thinkingLevel,
|
||||
validatorThinkingLevel,
|
||||
planningThinkingLevel,
|
||||
mergerThinkingLevel,
|
||||
plannerOversightLevel,
|
||||
summarize,
|
||||
reviewLevel,
|
||||
@@ -317,9 +327,12 @@ export function updateTask(
|
||||
validatorModelId?: string | null;
|
||||
planningModelProvider?: string | null;
|
||||
planningModelId?: string | null;
|
||||
mergerModelProvider?: string | null;
|
||||
mergerModelId?: string | null;
|
||||
thinkingLevel?: string | null;
|
||||
validatorThinkingLevel?: string | null;
|
||||
planningThinkingLevel?: string | null;
|
||||
mergerThinkingLevel?: string | null;
|
||||
plannerOversightLevel?: "off" | "observe" | "steer" | "autonomous" | null;
|
||||
/** FNXC:PlannerOversight 2026-07-14-18:11: boolean override or null to inherit project default. */
|
||||
sessionAdvisorEnabled?: boolean | null;
|
||||
|
||||
@@ -123,8 +123,13 @@ export function InlineCreateCard({
|
||||
const [validatorModelId, setValidatorModelId] = useState<string | undefined>(undefined);
|
||||
const [planningProvider, setPlanningProvider] = useState<string | undefined>(undefined);
|
||||
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
|
||||
const [mergerProvider, setMergerProvider] = useState<string | undefined>(undefined);
|
||||
const [mergerModelId, setMergerModelId] = useState<string | undefined>(undefined);
|
||||
/* FNXC:Settings-ThinkingLevel 2026-07-09-00:00: quick-create board card carries the same per-task thinking-level override as the full New Task modal; "" means "use default". */
|
||||
const [thinkingLevel, setThinkingLevel] = useState<string>("");
|
||||
const [validatorThinkingLevel, setValidatorThinkingLevel] = useState<string>("");
|
||||
const [planningThinkingLevel, setPlanningThinkingLevel] = useState<string>("");
|
||||
const [mergerThinkingLevel, setMergerThinkingLevel] = useState<string>("");
|
||||
const [optionalSteps, setOptionalSteps] = useState<ResolvedWorkflowOptionalStep[]>([]);
|
||||
const [enabledOptionalStepIds, setEnabledOptionalStepIds] = useState<string[]>([]);
|
||||
const [priority, setPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY);
|
||||
@@ -288,7 +293,8 @@ export function InlineCreateCard({
|
||||
const hasExecutorOverride = Boolean(executorProvider && executorModelId);
|
||||
const hasValidatorOverride = Boolean(validatorProvider && validatorModelId);
|
||||
const hasPlanningOverride = Boolean(planningProvider && planningModelId);
|
||||
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride);
|
||||
const hasMergerOverride = Boolean(mergerProvider && mergerModelId);
|
||||
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride) + Number(hasMergerOverride);
|
||||
const effectiveWorkflowId = selectedWorkflowId || settings?.defaultWorkflowId || "builtin:coding";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -436,7 +442,12 @@ export function InlineCreateCard({
|
||||
setValidatorModelId(undefined);
|
||||
setPlanningProvider(undefined);
|
||||
setPlanningModelId(undefined);
|
||||
setMergerProvider(undefined);
|
||||
setMergerModelId(undefined);
|
||||
setThinkingLevel("");
|
||||
setValidatorThinkingLevel("");
|
||||
setPlanningThinkingLevel("");
|
||||
setMergerThinkingLevel("");
|
||||
setEnabledOptionalStepIds([]);
|
||||
setPriority(DEFAULT_TASK_PRIORITY);
|
||||
setDependencies([]);
|
||||
@@ -494,6 +505,11 @@ export function InlineCreateCard({
|
||||
validatorModelId: hasValidatorOverride ? validatorModelId : undefined,
|
||||
planningModelProvider: hasPlanningOverride ? planningProvider : undefined,
|
||||
planningModelId: hasPlanningOverride ? planningModelId : undefined,
|
||||
mergerModelProvider: hasMergerOverride ? mergerProvider : undefined,
|
||||
mergerModelId: hasMergerOverride ? mergerModelId : undefined,
|
||||
validatorThinkingLevel: validatorThinkingLevel !== "" ? validatorThinkingLevel as ThinkingLevel : undefined,
|
||||
planningThinkingLevel: planningThinkingLevel !== "" ? planningThinkingLevel as ThinkingLevel : undefined,
|
||||
mergerThinkingLevel: mergerThinkingLevel !== "" ? mergerThinkingLevel as ThinkingLevel : undefined,
|
||||
thinkingLevel: thinkingLevel !== "" ? (thinkingLevel as ThinkingLevel) : undefined,
|
||||
/*
|
||||
FNXC:InlineCreateWorkflowSteps 2026-06-29-02:45:
|
||||
@@ -516,7 +532,7 @@ export function InlineCreateCard({
|
||||
}
|
||||
|
||||
await submitTask(input);
|
||||
}, [description, submitting, selectedWorkflowId, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, thinkingLevel, optionalSteps.length, enabledOptionalStepIds, priority, effectiveNodeId, projectId, addToast, submitTask]);
|
||||
}, [description, submitting, selectedWorkflowId, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, hasMergerOverride, mergerProvider, mergerModelId, thinkingLevel, validatorThinkingLevel, planningThinkingLevel, mergerThinkingLevel, optionalSteps.length, enabledOptionalStepIds, priority, effectiveNodeId, projectId, addToast, submitTask]);
|
||||
|
||||
const handleDuplicateProceed = useCallback(async () => {
|
||||
const matches = duplicateMatches;
|
||||
@@ -739,7 +755,12 @@ export function InlineCreateCard({
|
||||
setValidatorModelId(undefined);
|
||||
setPlanningProvider(undefined);
|
||||
setPlanningModelId(undefined);
|
||||
setMergerProvider(undefined);
|
||||
setMergerModelId(undefined);
|
||||
setThinkingLevel("");
|
||||
setValidatorThinkingLevel("");
|
||||
setPlanningThinkingLevel("");
|
||||
setMergerThinkingLevel("");
|
||||
setEnabledOptionalStepIds([]);
|
||||
setSelectedPresetId(undefined);
|
||||
setSelectedAgentId(null);
|
||||
@@ -1214,9 +1235,17 @@ export function InlineCreateCard({
|
||||
executorValue={executorSelectionValue}
|
||||
validatorValue={validatorSelectionValue}
|
||||
planningValue={planningSelectionValue}
|
||||
mergerValue={getModelSelectionValue(mergerProvider, mergerModelId)}
|
||||
onExecutorChange={handleExecutorChange}
|
||||
onValidatorChange={handleValidatorChange}
|
||||
onPlanningChange={handlePlanningModelChange}
|
||||
onMergerChange={(value) => { const next = parseModelSelection(value); setMergerProvider(next.provider); setMergerModelId(next.modelId); }}
|
||||
mergerThinkingLevel={mergerThinkingLevel}
|
||||
onMergerThinkingLevelChange={setMergerThinkingLevel}
|
||||
validatorThinkingLevel={validatorThinkingLevel}
|
||||
onValidatorThinkingLevelChange={setValidatorThinkingLevel}
|
||||
planningThinkingLevel={planningThinkingLevel}
|
||||
onPlanningThinkingLevelChange={setPlanningThinkingLevel}
|
||||
thinkingLevel={thinkingLevel}
|
||||
onThinkingLevelChange={handleThinkingLevelChange}
|
||||
defaultThinkingLevel={settings?.defaultThinkingLevel}
|
||||
|
||||
@@ -17,9 +17,17 @@ interface ModelSelectionModalProps {
|
||||
executorValue: string;
|
||||
validatorValue: string;
|
||||
planningValue?: string;
|
||||
mergerValue?: string;
|
||||
onExecutorChange: (value: string) => void;
|
||||
onValidatorChange: (value: string) => void;
|
||||
onPlanningChange?: (value: string) => void;
|
||||
onMergerChange?: (value: string) => void;
|
||||
mergerThinkingLevel?: string;
|
||||
onMergerThinkingLevelChange?: (value: string) => void;
|
||||
validatorThinkingLevel?: string;
|
||||
onValidatorThinkingLevelChange?: (value: string) => void;
|
||||
planningThinkingLevel?: string;
|
||||
onPlanningThinkingLevelChange?: (value: string) => void;
|
||||
/** Current thinking-level override, or "" for the effective default. Optional so pre-existing callers keep compiling unchanged. */
|
||||
thinkingLevel?: string;
|
||||
/**
|
||||
@@ -64,9 +72,17 @@ export function ModelSelectionModal({
|
||||
executorValue,
|
||||
validatorValue,
|
||||
planningValue = "",
|
||||
mergerValue = "",
|
||||
onExecutorChange,
|
||||
onValidatorChange,
|
||||
onPlanningChange,
|
||||
onMergerChange,
|
||||
mergerThinkingLevel = "",
|
||||
onMergerThinkingLevelChange,
|
||||
validatorThinkingLevel = "",
|
||||
onValidatorThinkingLevelChange,
|
||||
planningThinkingLevel = "",
|
||||
onPlanningThinkingLevelChange,
|
||||
thinkingLevel = "",
|
||||
onThinkingLevelChange,
|
||||
defaultThinkingLevel,
|
||||
@@ -173,6 +189,7 @@ export function ModelSelectionModal({
|
||||
const hasExecutorOverride = Boolean(executorValue);
|
||||
const hasValidatorOverride = Boolean(validatorValue);
|
||||
const hasPlanningOverride = Boolean(planningValue);
|
||||
const hasMergerOverride = Boolean(mergerValue);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open" onClick={handleOverlayClick} role="dialog" aria-modal="true" data-testid="model-selection-modal">
|
||||
@@ -269,6 +286,9 @@ export function ModelSelectionModal({
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
thinkingLevel={planningThinkingLevel}
|
||||
onThinkingLevelChange={onPlanningThinkingLevelChange}
|
||||
defaultThinkingLevel={defaultThinkingLevel ?? "off"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,6 +323,12 @@ export function ModelSelectionModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{onMergerChange ? <div className="task-detail-section"><div className="inline-create-model-row">
|
||||
<label htmlFor="model-selection-merger" className="inline-create-model-label">{t("tasks.mergerModel", "Merger Model")}</label>
|
||||
<span className={`model-badge ${hasMergerOverride ? "model-badge-custom" : "model-badge-default"}`}>{getModelBadgeLabel(models, mergerValue, t)}</span>
|
||||
<CustomModelDropdown id="model-selection-merger" label={t("tasks.mergerModel", "Merger Model")} value={mergerValue} onChange={onMergerChange} models={models} placeholder={t("tasks.usingDefault", "Using default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} thinkingLevel={mergerThinkingLevel} onThinkingLevelChange={onMergerThinkingLevelChange} defaultThinkingLevel={defaultThinkingLevel ?? "off"} />
|
||||
</div></div> : null}
|
||||
|
||||
<div className="task-detail-section">
|
||||
<div className="inline-create-model-row">
|
||||
<label htmlFor="model-selection-validator" className="inline-create-model-label">
|
||||
@@ -325,6 +351,9 @@ export function ModelSelectionModal({
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
thinkingLevel={validatorThinkingLevel}
|
||||
onThinkingLevelChange={onValidatorThinkingLevelChange}
|
||||
defaultThinkingLevel={defaultThinkingLevel ?? "off"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveTaskMergerModel,
|
||||
} from "@fusion/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useFavorites } from "../hooks/useFavorites";
|
||||
@@ -54,6 +55,13 @@ function getPlanningSelection(task: Task | TaskDetail): ModelSelection {
|
||||
};
|
||||
}
|
||||
|
||||
function getMergerSelection(task: Task | TaskDetail): ModelSelection {
|
||||
return {
|
||||
provider: normalizeModelField(task.mergerModelProvider),
|
||||
modelId: normalizeModelField(task.mergerModelId),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveEffectiveExecutor(
|
||||
task: Task | TaskDetail,
|
||||
settings?: Settings,
|
||||
@@ -75,6 +83,13 @@ function resolveEffectivePlanning(
|
||||
return resolveTaskPlanningModel(task, settings);
|
||||
}
|
||||
|
||||
function resolveEffectiveMerger(
|
||||
task: Task | TaskDetail,
|
||||
settings?: Settings,
|
||||
): ModelSelection {
|
||||
return resolveTaskMergerModel(task, settings);
|
||||
}
|
||||
|
||||
function parseModelValue(value: string): ModelSelection {
|
||||
if (!value) {
|
||||
return { provider: undefined, modelId: undefined };
|
||||
@@ -98,7 +113,7 @@ function selectionsEqual(a: ModelSelection, b: ModelSelection): boolean {
|
||||
}
|
||||
|
||||
function getSuccessToastMessage(
|
||||
target: "executor" | "validator" | "planning",
|
||||
target: "executor" | "validator" | "planning" | "merger",
|
||||
selection: ModelSelection,
|
||||
t: (key: string, defaultValue: string, options?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
@@ -106,6 +121,7 @@ function getSuccessToastMessage(
|
||||
executor: { key: "models.targetLabels.executor", defaultValue: "Executor" },
|
||||
validator: { key: "models.targetLabels.validator", defaultValue: "Reviewer" },
|
||||
planning: { key: "models.targetLabels.planning", defaultValue: "Planning" },
|
||||
merger: { key: "tasks.mergerModel", defaultValue: "Merger" },
|
||||
};
|
||||
const labelEntry = labelKeys[target] ?? { key: `models.targetLabels.${target}`, defaultValue: target };
|
||||
const label = t(labelEntry.key, labelEntry.defaultValue);
|
||||
@@ -138,13 +154,17 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
const [savedValidator, setSavedValidator] = useState<ModelSelection>(() => getValidatorSelection(task));
|
||||
const [selectedPlanning, setSelectedPlanning] = useState<ModelSelection>(() => getPlanningSelection(task));
|
||||
const [savedPlanning, setSavedPlanning] = useState<ModelSelection>(() => getPlanningSelection(task));
|
||||
const [selectedMerger, setSelectedMerger] = useState<ModelSelection>(() => getMergerSelection(task));
|
||||
const [savedMerger, setSavedMerger] = useState<ModelSelection>(() => getMergerSelection(task));
|
||||
const [selectedThinking, setSelectedThinking] = useState<string | null>(() => task.thinkingLevel ?? null);
|
||||
const [savedThinking, setSavedThinking] = useState<string | null>(() => task.thinkingLevel ?? null);
|
||||
const [selectedValidatorThinking, setSelectedValidatorThinking] = useState<string | null>(() => task.validatorThinkingLevel ?? null);
|
||||
const [savedValidatorThinking, setSavedValidatorThinking] = useState<string | null>(() => task.validatorThinkingLevel ?? null);
|
||||
const [selectedPlanningThinking, setSelectedPlanningThinking] = useState<string | null>(() => task.planningThinkingLevel ?? null);
|
||||
const [savedPlanningThinking, setSavedPlanningThinking] = useState<string | null>(() => task.planningThinkingLevel ?? null);
|
||||
const [savingTarget, setSavingTarget] = useState<"executor" | "validator" | "planning" | "thinking" | null>(null);
|
||||
const [selectedMergerThinking, setSelectedMergerThinking] = useState<string | null>(() => task.mergerThinkingLevel ?? null);
|
||||
const [savedMergerThinking, setSavedMergerThinking] = useState<string | null>(() => task.mergerThinkingLevel ?? null);
|
||||
const [savingTarget, setSavingTarget] = useState<"executor" | "validator" | "planning" | "merger" | "thinking" | null>(null);
|
||||
|
||||
const activeTaskIdRef = useRef(task.id);
|
||||
|
||||
@@ -170,6 +190,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
const nextExecutor = getExecutorSelection(task);
|
||||
const nextValidator = getValidatorSelection(task);
|
||||
const nextPlanning = getPlanningSelection(task);
|
||||
const nextMerger = getMergerSelection(task);
|
||||
|
||||
setSelectedExecutor(nextExecutor);
|
||||
setSavedExecutor(nextExecutor);
|
||||
@@ -177,32 +198,40 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
setSavedValidator(nextValidator);
|
||||
setSelectedPlanning(nextPlanning);
|
||||
setSavedPlanning(nextPlanning);
|
||||
setSelectedMerger(nextMerger);
|
||||
setSavedMerger(nextMerger);
|
||||
const nextThinking = task.thinkingLevel ?? null;
|
||||
const nextValidatorThinking = task.validatorThinkingLevel ?? null;
|
||||
const nextPlanningThinking = task.planningThinkingLevel ?? null;
|
||||
const nextMergerThinking = task.mergerThinkingLevel ?? null;
|
||||
setSelectedThinking(nextThinking);
|
||||
setSavedThinking(nextThinking);
|
||||
setSelectedValidatorThinking(nextValidatorThinking);
|
||||
setSavedValidatorThinking(nextValidatorThinking);
|
||||
setSelectedPlanningThinking(nextPlanningThinking);
|
||||
setSavedPlanningThinking(nextPlanningThinking);
|
||||
setSelectedMergerThinking(nextMergerThinking);
|
||||
setSavedMergerThinking(nextMergerThinking);
|
||||
setSavingTarget(null);
|
||||
}, [task.id, task.modelProvider, task.modelId, task.validatorModelProvider, task.validatorModelId, task.planningModelProvider, task.planningModelId, task.thinkingLevel, task.validatorThinkingLevel, task.planningThinkingLevel]);
|
||||
}, [task.id, task.modelProvider, task.modelId, task.validatorModelProvider, task.validatorModelId, task.planningModelProvider, task.planningModelId, task.mergerModelProvider, task.mergerModelId, task.thinkingLevel, task.validatorThinkingLevel, task.planningThinkingLevel, task.mergerThinkingLevel]);
|
||||
|
||||
const executorValue = useMemo(() => getDropdownValue(selectedExecutor), [selectedExecutor]);
|
||||
const validatorValue = useMemo(() => getDropdownValue(selectedValidator), [selectedValidator]);
|
||||
const planningValue = useMemo(() => getDropdownValue(selectedPlanning), [selectedPlanning]);
|
||||
const mergerValue = useMemo(() => getDropdownValue(selectedMerger), [selectedMerger]);
|
||||
const effectiveExecutor = useMemo(() => resolveEffectiveExecutor(task, settings), [task, settings]);
|
||||
const effectiveValidator = useMemo(() => resolveEffectiveValidator(task, settings), [task, settings]);
|
||||
const effectivePlanning = useMemo(() => resolveEffectivePlanning(task, settings), [task, settings]);
|
||||
const effectiveMerger = useMemo(() => resolveEffectiveMerger(task, settings), [task, settings]);
|
||||
const isSaving = savingTarget !== null;
|
||||
|
||||
const saveSelection = useCallback(
|
||||
async (target: "executor" | "validator" | "planning", nextSelection: ModelSelection) => {
|
||||
async (target: "executor" | "validator" | "planning" | "merger", nextSelection: ModelSelection) => {
|
||||
const requestTaskId = task.id;
|
||||
const previousSavedExecutor = savedExecutor;
|
||||
const previousSavedValidator = savedValidator;
|
||||
const previousSavedPlanning = savedPlanning;
|
||||
const previousSavedMerger = savedMerger;
|
||||
|
||||
setSavingTarget(target);
|
||||
|
||||
@@ -218,7 +247,12 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
validatorModelProvider: nextSelection.provider ?? null,
|
||||
validatorModelId: nextSelection.modelId ?? null,
|
||||
}
|
||||
: {
|
||||
: target === "merger"
|
||||
? {
|
||||
mergerModelProvider: nextSelection.provider ?? null,
|
||||
mergerModelId: nextSelection.modelId ?? null,
|
||||
}
|
||||
: {
|
||||
planningModelProvider: nextSelection.provider ?? null,
|
||||
planningModelId: nextSelection.modelId ?? null,
|
||||
})
|
||||
@@ -229,6 +263,8 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
validatorModelId: (target === "validator" ? nextSelection : savedValidator).modelId ?? null,
|
||||
planningModelProvider: (target === "planning" ? nextSelection : savedPlanning).provider ?? null,
|
||||
planningModelId: (target === "planning" ? nextSelection : savedPlanning).modelId ?? null,
|
||||
mergerModelProvider: (target === "merger" ? nextSelection : savedMerger).provider ?? null,
|
||||
mergerModelId: (target === "merger" ? nextSelection : savedMerger).modelId ?? null,
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -244,6 +280,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
const nextSavedExecutor = getExecutorSelection(updatedTask);
|
||||
const nextSavedValidator = getValidatorSelection(updatedTask);
|
||||
const nextSavedPlanning = getPlanningSelection(updatedTask);
|
||||
const nextSavedMerger = getMergerSelection(updatedTask);
|
||||
|
||||
setSavedExecutor(nextSavedExecutor);
|
||||
setSelectedExecutor(nextSavedExecutor);
|
||||
@@ -251,12 +288,15 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
setSelectedValidator(nextSavedValidator);
|
||||
setSavedPlanning(nextSavedPlanning);
|
||||
setSelectedPlanning(nextSavedPlanning);
|
||||
setSavedMerger(nextSavedMerger);
|
||||
setSelectedMerger(nextSavedMerger);
|
||||
onTaskUpdated?.(updatedTask);
|
||||
|
||||
const targetSelections: Record<string, ModelSelection> = {
|
||||
executor: nextSavedExecutor,
|
||||
validator: nextSavedValidator,
|
||||
planning: nextSavedPlanning,
|
||||
merger: nextSavedMerger,
|
||||
};
|
||||
|
||||
addToast(
|
||||
@@ -272,8 +312,10 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
setSelectedExecutor(previousSavedExecutor);
|
||||
} else if (target === "validator") {
|
||||
setSelectedValidator(previousSavedValidator);
|
||||
} else {
|
||||
} else if (target === "planning") {
|
||||
setSelectedPlanning(previousSavedPlanning);
|
||||
} else {
|
||||
setSelectedMerger(previousSavedMerger);
|
||||
}
|
||||
|
||||
addToast(getErrorMessage(err) || t("models.errors.failedSaveSettings", "Failed to save model settings"), "error");
|
||||
@@ -283,7 +325,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
}
|
||||
}
|
||||
},
|
||||
[task.id, savedExecutor, savedValidator, savedPlanning, addToast, onTaskUpdated, projectId, t],
|
||||
[task.id, savedExecutor, savedValidator, savedPlanning, savedMerger, addToast, onTaskUpdated, projectId, t],
|
||||
);
|
||||
|
||||
const handleExecutorChange = useCallback(
|
||||
@@ -328,6 +370,15 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
[savedPlanning, saveSelection],
|
||||
);
|
||||
|
||||
const handleMergerChange = useCallback(
|
||||
(value: string) => {
|
||||
const nextSelection = parseModelValue(value);
|
||||
setSelectedMerger(nextSelection);
|
||||
if (!selectionsEqual(nextSelection, savedMerger)) void saveSelection("merger", nextSelection);
|
||||
},
|
||||
[savedMerger, saveSelection],
|
||||
);
|
||||
|
||||
const handleThinkingChange = useCallback(
|
||||
async (value: string) => {
|
||||
const requestTaskId = task.id;
|
||||
@@ -482,13 +533,36 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
[task.id, savedPlanningThinking, settings, addToast, onTaskUpdated, projectId, t],
|
||||
);
|
||||
|
||||
const handleMergerThinkingChange = useCallback(async (value: string) => {
|
||||
const requestTaskId = task.id;
|
||||
const previousThinking = savedMergerThinking;
|
||||
const nextValue = value === "" ? null : value;
|
||||
setSelectedMergerThinking(nextValue);
|
||||
setSavingTarget("thinking");
|
||||
try {
|
||||
const updatedTask = await updateTask(requestTaskId, { mergerThinkingLevel: nextValue }, projectId);
|
||||
if (activeTaskIdRef.current !== requestTaskId) return;
|
||||
const nextThinking = updatedTask.mergerThinkingLevel ?? null;
|
||||
setSavedMergerThinking(nextThinking);
|
||||
setSelectedMergerThinking(nextThinking);
|
||||
onTaskUpdated?.(updatedTask);
|
||||
} catch (err) {
|
||||
if (activeTaskIdRef.current !== requestTaskId) return;
|
||||
setSelectedMergerThinking(previousThinking);
|
||||
addToast(getErrorMessage(err) || t("models.errors.failedSaveThinking", "Failed to save thinking level"), "error");
|
||||
} finally {
|
||||
if (activeTaskIdRef.current === requestTaskId) setSavingTarget(null);
|
||||
}
|
||||
}, [task.id, savedMergerThinking, addToast, onTaskUpdated, projectId, t]);
|
||||
|
||||
/*
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-13-00:27:
|
||||
* Reviewer and Planning task-detail model boxes carry independent per-lane reasoning-effort overrides persisted to task.validatorThinkingLevel and task.planningThinkingLevel while the Executor box continues to use task.thinkingLevel.
|
||||
* Reviewer, Planning, and Merger task-detail model boxes carry independent per-lane reasoning-effort overrides. Merger selection reuses CustomModelDropdown and remains independent of its provider/model pair.
|
||||
*/
|
||||
const executorUsingDefault = !savedExecutor.provider && !savedExecutor.modelId;
|
||||
const validatorUsingDefault = !savedValidator.provider && !savedValidator.modelId;
|
||||
const planningUsingDefault = !savedPlanning.provider && !savedPlanning.modelId;
|
||||
const mergerUsingDefault = !savedMerger.provider && !savedMerger.modelId;
|
||||
|
||||
return (
|
||||
<div className="model-selector-tab">
|
||||
@@ -605,8 +679,17 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
</div>
|
||||
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="mergerModel">{t("tasks.mergerModel", "Merger Model")}</label>
|
||||
<div className="model-selector-current">
|
||||
{mergerUsingDefault ? <span className="model-badge model-badge-default">{t("models.states.usingDefault", "Using default")}{effectiveMerger.provider && effectiveMerger.modelId ? ` (${effectiveMerger.provider}/${effectiveMerger.modelId})` : ""}</span> : <span className="model-badge model-badge-custom">{savedMerger.provider && <ProviderIcon provider={savedMerger.provider} size="sm" />}{savedMerger.provider}/{savedMerger.modelId}</span>}
|
||||
</div>
|
||||
<CustomModelDropdown id="mergerModel" label={t("tasks.mergerModel", "Merger Model")} value={mergerValue} onChange={handleMergerChange} models={availableModels} disabled={isSaving} placeholder={t("tasks.usingDefault", "Using default")} favoriteProviders={favoriteProviders} onToggleFavorite={handleToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={handleToggleModelFavorite} thinkingLevel={selectedMergerThinking ?? ""} onThinkingLevelChange={handleMergerThinkingChange} defaultThinkingLevel={settings?.mergerThinkingLevel ?? "off"} />
|
||||
<small>{t("models.descriptions.merger", "The AI model used to merge this task.")}</small>
|
||||
</div>
|
||||
|
||||
<div className="model-selector-status">
|
||||
{executorUsingDefault && validatorUsingDefault && planningUsingDefault && savedThinking === null && savedValidatorThinking === null && savedPlanningThinking === null
|
||||
{executorUsingDefault && validatorUsingDefault && planningUsingDefault && mergerUsingDefault && savedThinking === null && savedValidatorThinking === null && savedPlanningThinking === null && savedMergerThinking === null
|
||||
? t("models.messages.usingDefaults", "Using project or global default models.")
|
||||
: t("models.messages.upToDate", "Model settings are up to date.")}
|
||||
</div>
|
||||
|
||||
@@ -172,15 +172,21 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const [showPriorityPicker, setShowPriorityPicker] = useState(false);
|
||||
const [agentsLoading, setAgentsLoading] = useState(false);
|
||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||
const [activeModelSubmenu, setActiveModelSubmenu] = useState<"plan" | "executor" | "validator" | null>(null);
|
||||
const [activeModelSubmenu, setActiveModelSubmenu] = useState<"plan" | "executor" | "validator" | "merger" | null>(null);
|
||||
const [executorProvider, setExecutorProvider] = useState<string | undefined>(undefined);
|
||||
const [executorModelId, setExecutorModelId] = useState<string | undefined>(undefined);
|
||||
const [validatorProvider, setValidatorProvider] = useState<string | undefined>(undefined);
|
||||
const [validatorModelId, setValidatorModelId] = useState<string | undefined>(undefined);
|
||||
const [planningProvider, setPlanningProvider] = useState<string | undefined>(undefined);
|
||||
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
|
||||
const [mergerProvider, setMergerProvider] = useState<string | undefined>(undefined);
|
||||
const [mergerModelId, setMergerModelId] = useState<string | undefined>(undefined);
|
||||
/* FNXC:Settings-ThinkingLevel 2026-07-09-00:00: inline quick-entry bar carries the same per-task thinking-level override as the full New Task modal; "" means "use default". */
|
||||
const [thinkingLevel, setThinkingLevel] = useState<string>("");
|
||||
const [validatorThinkingLevel, setValidatorThinkingLevel] = useState<string>("");
|
||||
const [planningThinkingLevel, setPlanningThinkingLevel] = useState<string>("");
|
||||
const [mergerThinkingLevel, setMergerThinkingLevel] = useState<string>("");
|
||||
/* FNXC:QuickAddModels 2026-07-16-12:00: Quick Add reuses CustomModelDropdown for merger and each lane's independent thinking override. */
|
||||
const modelTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const modelMenuPortalRef = useRef<HTMLDivElement>(null);
|
||||
const agentPickerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -415,11 +421,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const executorSelectionValue = getModelSelectionValue(executorProvider, executorModelId);
|
||||
const validatorSelectionValue = getModelSelectionValue(validatorProvider, validatorModelId);
|
||||
const planningSelectionValue = getModelSelectionValue(planningProvider, planningModelId);
|
||||
const mergerSelectionValue = getModelSelectionValue(mergerProvider, mergerModelId);
|
||||
|
||||
const hasExecutorOverride = Boolean(executorProvider && executorModelId);
|
||||
const hasValidatorOverride = Boolean(validatorProvider && validatorModelId);
|
||||
const hasPlanningOverride = Boolean(planningProvider && planningModelId);
|
||||
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride);
|
||||
const hasMergerOverride = Boolean(mergerProvider && mergerModelId);
|
||||
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride) + Number(hasMergerOverride);
|
||||
const modelMenuLabel = selectedPresetId
|
||||
? settings?.modelPresets?.find((p) => p.id === selectedPresetId)?.name ?? t("tasks.models", "Models")
|
||||
: selectedModelCount > 0
|
||||
@@ -613,7 +621,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
setValidatorModelId(undefined);
|
||||
setPlanningProvider(undefined);
|
||||
setPlanningModelId(undefined);
|
||||
setMergerProvider(undefined);
|
||||
setMergerModelId(undefined);
|
||||
setThinkingLevel("");
|
||||
setValidatorThinkingLevel("");
|
||||
setPlanningThinkingLevel("");
|
||||
setMergerThinkingLevel("");
|
||||
setSelectedPresetId(undefined);
|
||||
setEnabledOptionalStepIds(optionalSteps.filter((step) => step.defaultOn).map((step) => step.templateId));
|
||||
setIsFastMode(false);
|
||||
@@ -739,6 +752,11 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
validatorModelId: hasValidatorOverride ? validatorModelId : undefined,
|
||||
planningModelProvider: hasPlanningOverride ? planningProvider : undefined,
|
||||
planningModelId: hasPlanningOverride ? planningModelId : undefined,
|
||||
mergerModelProvider: hasMergerOverride ? mergerProvider : undefined,
|
||||
mergerModelId: hasMergerOverride ? mergerModelId : undefined,
|
||||
validatorThinkingLevel: validatorThinkingLevel !== "" ? (validatorThinkingLevel as ThinkingLevel) : undefined,
|
||||
planningThinkingLevel: planningThinkingLevel !== "" ? (planningThinkingLevel as ThinkingLevel) : undefined,
|
||||
mergerThinkingLevel: mergerThinkingLevel !== "" ? (mergerThinkingLevel as ThinkingLevel) : undefined,
|
||||
thinkingLevel: thinkingLevel !== "" ? (thinkingLevel as ThinkingLevel) : undefined,
|
||||
/*
|
||||
FNXC:QuickAddWorkflowSteps 2026-06-29-01:31:
|
||||
@@ -1510,8 +1528,11 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
setValidatorModelId(next.modelId);
|
||||
}, []);
|
||||
|
||||
const handleThinkingLevelChange = useCallback((value: string) => {
|
||||
setThinkingLevel(value);
|
||||
const handleThinkingLevelChange = useCallback((value: string) => setThinkingLevel(value), []);
|
||||
const handleMergerModelChange = useCallback((value: string) => {
|
||||
const next = parseModelSelection(value);
|
||||
setMergerProvider(next.provider);
|
||||
setMergerModelId(next.modelId);
|
||||
}, []);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
@@ -2425,6 +2446,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
</span>
|
||||
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
||||
</button>
|
||||
<button type="button" className={`model-menu-item ${hasMergerOverride ? "model-menu-item--active" : ""}`} onClick={() => setActiveModelSubmenu("merger")} data-testid="model-menu-merger">
|
||||
<span className="model-menu-item-label"><Brain size={12} /> {t("tasks.mergerModel", "Merger Model")}</span>
|
||||
<span className="model-menu-item-value">{hasMergerOverride ? getModelBadgeLabel(mergerProvider, mergerModelId) : t("tasks.usingDefault", "Using default")}</span><ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
// Submenu with CustomModelDropdown for the selected target
|
||||
@@ -2442,6 +2467,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
{activeModelSubmenu === "plan" && t("tasks.planModel", "Plan Model")}
|
||||
{activeModelSubmenu === "executor" && t("tasks.executorModel", "Executor Model")}
|
||||
{activeModelSubmenu === "validator" && t("tasks.reviewerModel", "Reviewer Model")}
|
||||
{activeModelSubmenu === "merger" && t("tasks.mergerModel", "Merger Model")}
|
||||
</div>
|
||||
<CustomModelDropdown
|
||||
models={loadedModels}
|
||||
@@ -2450,14 +2476,14 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
? planningSelectionValue
|
||||
: activeModelSubmenu === "executor"
|
||||
? executorSelectionValue
|
||||
: validatorSelectionValue
|
||||
: activeModelSubmenu === "validator" ? validatorSelectionValue : mergerSelectionValue
|
||||
}
|
||||
onChange={
|
||||
activeModelSubmenu === "plan"
|
||||
? handlePlanningModelChange
|
||||
: activeModelSubmenu === "executor"
|
||||
? handleExecutorChange
|
||||
: handleValidatorChange
|
||||
: activeModelSubmenu === "validator" ? handleValidatorChange : handleMergerModelChange
|
||||
}
|
||||
placeholder={t("tasks.usingDefault", "Using default")}
|
||||
disabled={modelsLoading}
|
||||
@@ -2467,9 +2493,9 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={effectiveFavoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
thinkingLevel={activeModelSubmenu === "executor" ? thinkingLevel : undefined}
|
||||
onThinkingLevelChange={activeModelSubmenu === "executor" ? handleThinkingLevelChange : undefined}
|
||||
defaultThinkingLevel={activeModelSubmenu === "executor" ? settings?.defaultThinkingLevel ?? "off" : undefined}
|
||||
thinkingLevel={activeModelSubmenu === "executor" ? thinkingLevel : activeModelSubmenu === "plan" ? planningThinkingLevel : activeModelSubmenu === "validator" ? validatorThinkingLevel : mergerThinkingLevel}
|
||||
onThinkingLevelChange={activeModelSubmenu === "executor" ? handleThinkingLevelChange : activeModelSubmenu === "plan" ? setPlanningThinkingLevel : activeModelSubmenu === "validator" ? setValidatorThinkingLevel : setMergerThinkingLevel}
|
||||
defaultThinkingLevel={settings?.defaultThinkingLevel ?? "off"}
|
||||
/>
|
||||
{modelsError && (
|
||||
<div className="model-submenu-error">
|
||||
|
||||
@@ -180,6 +180,26 @@ describe("ModelSelectionModal", () => {
|
||||
expect(screen.getByTestId("mock-dropdown-model-selection-validator")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders merger and per-lane thinking controls through the shared dropdown", () => {
|
||||
const onMergerChange = vi.fn();
|
||||
const onMergerThinkingLevelChange = vi.fn();
|
||||
renderModelSelectionModal({
|
||||
onMergerChange,
|
||||
mergerValue: "anthropic/claude-sonnet-4-5",
|
||||
mergerThinkingLevel: "high",
|
||||
onMergerThinkingLevelChange,
|
||||
onValidatorThinkingLevelChange: vi.fn(),
|
||||
onPlanningChange: vi.fn(),
|
||||
onPlanningThinkingLevelChange: vi.fn(),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("mock-dropdown-model-selection-merger")).toBeTruthy();
|
||||
expect(screen.getByTestId("dropdown-value-model-selection-merger")).toHaveTextContent("anthropic/claude-sonnet-4-5");
|
||||
fireEvent.change(screen.getByTestId("dropdown-select-model-selection-merger"), { target: { value: "openai/gpt-4o" } });
|
||||
expect(onMergerChange).toHaveBeenCalledWith("openai/gpt-4o");
|
||||
expect(screen.getAllByTestId("custom-model-dropdown-thinking")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("calls onClose when clicking close button", () => {
|
||||
const onClose = vi.fn();
|
||||
renderModelSelectionModal({ onClose });
|
||||
|
||||
@@ -101,6 +101,8 @@ describe("ModelSelectorTab", () => {
|
||||
validatorModelId: "claude-haiku-5",
|
||||
planningModelProvider: "pi-claude-cli",
|
||||
planningModelId: "claude-haiku-5",
|
||||
mergerModelProvider: "pi-claude-cli",
|
||||
mergerModelId: "claude-haiku-5",
|
||||
thinkingLevel: "minimal",
|
||||
});
|
||||
|
||||
@@ -128,6 +130,11 @@ describe("ModelSelectorTab", () => {
|
||||
planningModelProvider: "pi-claude-cli",
|
||||
planningModelId: "claude-sonnet-5",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
...task,
|
||||
mergerModelProvider: "pi-claude-cli",
|
||||
mergerModelId: "claude-sonnet-5",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
...task,
|
||||
thinkingLevel: "high",
|
||||
@@ -139,6 +146,10 @@ describe("ModelSelectorTab", () => {
|
||||
.mockResolvedValueOnce({
|
||||
...task,
|
||||
planningThinkingLevel: "high",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
...task,
|
||||
mergerThinkingLevel: "high",
|
||||
});
|
||||
|
||||
render(
|
||||
@@ -176,10 +187,18 @@ describe("ModelSelectorTab", () => {
|
||||
}, "project-alpha");
|
||||
});
|
||||
|
||||
await selectDropdownOption(user, "Merger Model", "Claude Sonnet 5 (CLI)");
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(4, "FN-7398", {
|
||||
mergerModelProvider: "pi-claude-cli",
|
||||
mergerModelId: "claude-sonnet-5",
|
||||
}, "project-alpha");
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Executor Model/ }));
|
||||
await user.selectOptions(await screen.findByTestId("custom-model-dropdown-thinking"), "high");
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(4, "FN-7398", {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(5, "FN-7398", {
|
||||
thinkingLevel: "high",
|
||||
}, "project-alpha");
|
||||
});
|
||||
@@ -187,7 +206,7 @@ describe("ModelSelectorTab", () => {
|
||||
await user.click(screen.getByRole("button", { name: /Reviewer Model/ }));
|
||||
await user.selectOptions(await screen.findByTestId("custom-model-dropdown-thinking"), "high");
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(5, "FN-7398", {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(6, "FN-7398", {
|
||||
validatorThinkingLevel: "high",
|
||||
}, "project-alpha");
|
||||
});
|
||||
@@ -195,10 +214,18 @@ describe("ModelSelectorTab", () => {
|
||||
await user.click(screen.getByRole("button", { name: /Planning Model/ }));
|
||||
await user.selectOptions(await screen.findByTestId("custom-model-dropdown-thinking"), "high");
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(6, "FN-7398", {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(7, "FN-7398", {
|
||||
planningThinkingLevel: "high",
|
||||
}, "project-alpha");
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Merger Model/ }));
|
||||
await user.selectOptions(await screen.findByTestId("custom-model-dropdown-thinking"), "high");
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(8, "FN-7398", {
|
||||
mergerThinkingLevel: "high",
|
||||
}, "project-alpha");
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith(expect.stringContaining("set to"), "success");
|
||||
});
|
||||
|
||||
@@ -239,6 +266,7 @@ describe("ModelSelectorTab", () => {
|
||||
thinkingLevel: "medium",
|
||||
validatorThinkingLevel: "high",
|
||||
planningThinkingLevel: undefined,
|
||||
mergerThinkingLevel: "low",
|
||||
});
|
||||
|
||||
render(
|
||||
@@ -252,10 +280,11 @@ describe("ModelSelectorTab", () => {
|
||||
await waitFor(() => expect(screen.getByLabelText("Executor Model")).toBeInTheDocument());
|
||||
|
||||
const badges = screen.getAllByTestId("custom-model-dropdown-thinking-badge");
|
||||
expect(badges).toHaveLength(3);
|
||||
expect(badges).toHaveLength(4);
|
||||
expect(badges[0]).toHaveTextContent("Medium");
|
||||
expect(badges[1]).toHaveTextContent("High");
|
||||
expect(badges[2]).toHaveTextContent("Default (low)");
|
||||
expect(badges[3]).toHaveTextContent("Low");
|
||||
});
|
||||
|
||||
it("updates from a cached empty catalog to populated Claude CLI rows without remounting", async () => {
|
||||
|
||||
@@ -1104,7 +1104,12 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
validatorModelId,
|
||||
planningModelProvider,
|
||||
planningModelId,
|
||||
mergerModelProvider,
|
||||
mergerModelId,
|
||||
thinkingLevel,
|
||||
validatorThinkingLevel,
|
||||
planningThinkingLevel,
|
||||
mergerThinkingLevel,
|
||||
reviewLevel,
|
||||
executionMode,
|
||||
autoMerge,
|
||||
@@ -1142,11 +1147,15 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const validatedValidatorModelId = validateOptionalModelField(validatorModelId, "validatorModelId");
|
||||
const validatedPlanningModelProvider = validateOptionalModelField(planningModelProvider, "planningModelProvider");
|
||||
const validatedPlanningModelId = validateOptionalModelField(planningModelId, "planningModelId");
|
||||
const validatedMergerModelProvider = validateOptionalModelField(mergerModelProvider, "mergerModelProvider");
|
||||
const validatedMergerModelId = validateOptionalModelField(mergerModelId, "mergerModelId");
|
||||
|
||||
// Validate thinkingLevel if provided
|
||||
const validThinkingLevels = [...THINKING_LEVELS];
|
||||
if (thinkingLevel !== undefined && thinkingLevel !== null && !validThinkingLevels.includes(thinkingLevel)) {
|
||||
throw badRequest(`thinkingLevel must be one of: ${validThinkingLevels.join(", ")}`);
|
||||
for (const [name, value] of Object.entries({ thinkingLevel, validatorThinkingLevel, planningThinkingLevel, mergerThinkingLevel })) {
|
||||
if (value !== undefined && value !== null && !validThinkingLevels.includes(value as ThinkingLevel)) {
|
||||
throw badRequest(`${name} must be one of: ${validThinkingLevels.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate reviewLevel if provided (must be integer 0-3)
|
||||
@@ -1178,6 +1187,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const executorModel = normalizeModelSelectionPair(validatedModelProvider, validatedModelId);
|
||||
const validatorModel = normalizeModelSelectionPair(validatedValidatorModelProvider, validatedValidatorModelId);
|
||||
const planningModel = normalizeModelSelectionPair(validatedPlanningModelProvider, validatedPlanningModelId);
|
||||
const mergerModel = normalizeModelSelectionPair(validatedMergerModelProvider, validatedMergerModelId);
|
||||
|
||||
// Validate enabledWorkflowSteps if provided
|
||||
if (enabledWorkflowSteps !== undefined) {
|
||||
@@ -1486,7 +1496,12 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
validatorModelId: validatorModel.modelId ?? undefined,
|
||||
planningModelProvider: planningModel.provider ?? undefined,
|
||||
planningModelId: planningModel.modelId ?? undefined,
|
||||
mergerModelProvider: mergerModel.provider ?? undefined,
|
||||
mergerModelId: mergerModel.modelId ?? undefined,
|
||||
thinkingLevel: thinkingLevel || undefined,
|
||||
validatorThinkingLevel: validatorThinkingLevel || undefined,
|
||||
planningThinkingLevel: planningThinkingLevel || undefined,
|
||||
mergerThinkingLevel: mergerThinkingLevel || undefined,
|
||||
summarize,
|
||||
reviewLevel: reviewLevel ?? undefined,
|
||||
executionMode: executionMode || undefined,
|
||||
@@ -4371,7 +4386,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
router.patch("/tasks/:id", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const { title, description, prompt, priority, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, thinkingLevel, validatorThinkingLevel, planningThinkingLevel, assigneeUserId, reviewLevel, executionMode, sourceIssue, nodeId, branch, baseBranch, githubTracking, gitlabTracking, noCommitsExpected, autoMerge, overlapBlockedBy, status, dismissNearDuplicate, sessionAdvisorEnabled } = req.body;
|
||||
const { title, description, prompt, priority, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergerModelProvider, mergerModelId, thinkingLevel, validatorThinkingLevel, planningThinkingLevel, mergerThinkingLevel, assigneeUserId, reviewLevel, executionMode, sourceIssue, nodeId, branch, baseBranch, githubTracking, gitlabTracking, noCommitsExpected, autoMerge, overlapBlockedBy, status, dismissNearDuplicate, sessionAdvisorEnabled } = req.body;
|
||||
const hasBodyField = (field: string) => Object.prototype.hasOwnProperty.call(req.body, field);
|
||||
|
||||
// Validate model fields are strings or undefined/null
|
||||
@@ -4390,6 +4405,8 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const validatedValidatorModelId = validateModelField(validatorModelId, "validatorModelId");
|
||||
const validatedPlanningModelProvider = validateModelField(planningModelProvider, "planningModelProvider");
|
||||
const validatedPlanningModelId = validateModelField(planningModelId, "planningModelId");
|
||||
const validatedMergerModelProvider = validateModelField(mergerModelProvider, "mergerModelProvider");
|
||||
const validatedMergerModelId = validateModelField(mergerModelId, "mergerModelId");
|
||||
const validatedAssigneeUserId = validateModelField(assigneeUserId, "assigneeUserId");
|
||||
|
||||
// Validate thinking level fields if provided
|
||||
@@ -4402,6 +4419,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
validateThinkingLevel(thinkingLevel, "thinkingLevel");
|
||||
validateThinkingLevel(validatorThinkingLevel, "validatorThinkingLevel");
|
||||
validateThinkingLevel(planningThinkingLevel, "planningThinkingLevel");
|
||||
validateThinkingLevel(mergerThinkingLevel, "mergerThinkingLevel");
|
||||
|
||||
// Validate reviewLevel if provided (must be integer 0-3)
|
||||
if (reviewLevel !== undefined && reviewLevel !== null) {
|
||||
@@ -4683,9 +4701,12 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
if (hasBodyField("validatorModelId")) updates.validatorModelId = validatedValidatorModelId;
|
||||
if (hasBodyField("planningModelProvider")) updates.planningModelProvider = validatedPlanningModelProvider;
|
||||
if (hasBodyField("planningModelId")) updates.planningModelId = validatedPlanningModelId;
|
||||
if (hasBodyField("mergerModelProvider")) updates.mergerModelProvider = validatedMergerModelProvider;
|
||||
if (hasBodyField("mergerModelId")) updates.mergerModelId = validatedMergerModelId;
|
||||
if (hasBodyField("thinkingLevel")) updates.thinkingLevel = thinkingLevel === null ? null : thinkingLevel;
|
||||
if (hasBodyField("validatorThinkingLevel")) updates.validatorThinkingLevel = validatorThinkingLevel === null ? null : validatorThinkingLevel;
|
||||
if (hasBodyField("planningThinkingLevel")) updates.planningThinkingLevel = planningThinkingLevel === null ? null : planningThinkingLevel;
|
||||
if (hasBodyField("mergerThinkingLevel")) updates.mergerThinkingLevel = mergerThinkingLevel === null ? null : mergerThinkingLevel;
|
||||
if (hasBodyField("assigneeUserId")) updates.assigneeUserId = validatedAssigneeUserId;
|
||||
if (hasBodyField("reviewLevel")) updates.reviewLevel = reviewLevel;
|
||||
if (hasBodyField("executionMode")) updates.executionMode = executionMode === null ? null : executionMode;
|
||||
|
||||
@@ -124,6 +124,7 @@ describe("resolve model-lane thinking levels", () => {
|
||||
expect(resolveTitleSummarizerFallbackThinkingLevel({ defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "minimal" })).toBe("medium");
|
||||
expect(resolveTitleSummarizerFallbackThinkingLevel({ defaultThinkingLevel: "minimal" })).toBe("minimal");
|
||||
|
||||
expect(resolveMergerFallbackThinkingLevel({ mergerFallbackThinkingLevel: "xhigh", fallbackThinkingLevel: "high", mergerThinkingLevel: "medium" }, "minimal")).toBe("minimal");
|
||||
expect(resolveMergerFallbackThinkingLevel({ mergerFallbackThinkingLevel: "xhigh", fallbackThinkingLevel: "high", mergerThinkingLevel: "medium" })).toBe("xhigh");
|
||||
expect(resolveMergerFallbackThinkingLevel({ fallbackThinkingLevel: "high", defaultThinkingLevel: "low" })).toBe("high");
|
||||
expect(resolveMergerFallbackThinkingLevel({ defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "low" })).toBe("medium");
|
||||
@@ -341,6 +342,13 @@ describe("resolve session model parity", () => {
|
||||
}, staleRuntimeConfig)).toEqual({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
|
||||
});
|
||||
|
||||
it("prefers a complete per-task merger pair, ignores partial pairs, and still forces test mode", () => {
|
||||
const settings = { mergerProvider: "settings-provider", mergerModelId: "settings-model" };
|
||||
expect(resolveMergerSessionModel(settings, undefined, { mergerModelProvider: "task-provider", mergerModelId: "task-model" })).toEqual({ provider: "task-provider", modelId: "task-model" });
|
||||
expect(resolveMergerSessionModel(settings, undefined, { mergerModelProvider: "partial-provider" })).toEqual({ provider: "settings-provider", modelId: "settings-model" });
|
||||
expect(resolveMergerSessionModel({ ...settings, testMode: true }, undefined, { mergerModelProvider: "task-provider", mergerModelId: "task-model" })).toEqual({ provider: "mock", modelId: "scripted" });
|
||||
});
|
||||
|
||||
it("prefers the dedicated merger lane over default and other AI role lanes", () => {
|
||||
const staleRuntimeConfig = { model: "stale-provider/stale-model" };
|
||||
|
||||
|
||||
@@ -268,8 +268,12 @@ export function resolveTitleSummarizerThinkingLevel(settings: Partial<Settings>
|
||||
* thinking → global merger thinking → project default thinking override → global
|
||||
* default thinking. Unset at every level preserves prior default-only behavior.
|
||||
*/
|
||||
export function resolveMergerThinkingLevel(settings: Partial<Settings> | undefined): string | undefined {
|
||||
export function resolveMergerThinkingLevel(
|
||||
settings: Partial<Settings> | undefined,
|
||||
taskThinkingLevel?: ThinkingLevel | string,
|
||||
): string | undefined {
|
||||
return firstThinkingLevel(
|
||||
taskThinkingLevel,
|
||||
settings?.mergerThinkingLevel,
|
||||
settings?.mergerGlobalThinkingLevel,
|
||||
settings?.defaultThinkingLevelOverride,
|
||||
@@ -326,8 +330,10 @@ export function resolveTitleSummarizerFallbackThinkingLevel(settings: Partial<Se
|
||||
FNXC:Settings-MergerModel 2026-07-16-00:00:
|
||||
Merger recovery sessions use a lane-specific thinking level when configured, then preserve the former global-fallback and merger-primary inheritance order.
|
||||
*/
|
||||
export function resolveMergerFallbackThinkingLevel(settings: Partial<Settings> | undefined): string | undefined {
|
||||
export function resolveMergerFallbackThinkingLevel(settings: Partial<Settings> | undefined, taskThinkingLevel?: string | null): string | undefined {
|
||||
return firstThinkingLevel(
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: A task's merger thinking selection governs both the primary and fallback merger session, so fallback must not re-inherit a project/global lane value.
|
||||
taskThinkingLevel,
|
||||
settings?.mergerFallbackThinkingLevel,
|
||||
settings?.fallbackThinkingLevel,
|
||||
resolveMergerThinkingLevel(settings),
|
||||
@@ -590,6 +596,7 @@ export function resolveHeartbeatSessionModels(
|
||||
export function resolveMergerSessionModel(
|
||||
settings: Partial<Settings> | undefined,
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||
task?: { mergerModelProvider?: string | null; mergerModelId?: string | null },
|
||||
): { provider: string | undefined; modelId: string | undefined } {
|
||||
if (isTestModeActive(settings)) {
|
||||
return {
|
||||
@@ -604,7 +611,10 @@ export function resolveMergerSessionModel(
|
||||
not execution/planning/validator. Session fallback still uses the shared global
|
||||
fallbackProvider/fallbackModelId pair at createResolvedAgentSession call sites.
|
||||
*/
|
||||
const mergerModel = resolveMergerSettingsModel(settings);
|
||||
/* FNXC:Settings-MergerModel 2026-07-16-12:00: task pair → settings → global/default; partial task pairs inherit settings. */
|
||||
const mergerModel = task?.mergerModelProvider && task?.mergerModelId
|
||||
? { provider: task.mergerModelProvider, modelId: task.mergerModelId }
|
||||
: resolveMergerSettingsModel(settings);
|
||||
return pickSettingsThenRuntimeModel(mergerModel, assignedAgentRuntimeConfig);
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,8 @@ interface AgentDeps {
|
||||
/** Factory for a mutating AI agent bound to a fixed system prompt. */
|
||||
function makeMutatingAgent(store: TaskStore, settings: Settings, taskId: string, options: MergerOptions, audit: RunAuditor, systemPrompt: string) {
|
||||
return async (cwd: string, prompt: string): Promise<void> => {
|
||||
const model = resolveMergerSessionModel(settings);
|
||||
const task = await store.getTask(taskId).catch(() => undefined);
|
||||
const model = resolveMergerSessionModel(settings, undefined, task);
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: mutating merger retries resolve the project merger fallback lane before the shared global fallback.
|
||||
const mergerFallbackModel = resolveMergerFallbackModel(settings);
|
||||
const logger = new AgentLogger({
|
||||
@@ -413,8 +414,8 @@ function makeMutatingAgent(store: TaskStore, settings: Settings, taskId: string,
|
||||
defaultModelId: model.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, task?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings, task?.mergerThinkingLevel),
|
||||
runAuditor: audit,
|
||||
settings,
|
||||
// FNXC:McpConfig 2026-06-25-22:48: merger-ai is the production merge path, so the mutating agent resolves enabled MCP servers at session creation and relies on the shared runtime guard for unsupported providers.
|
||||
@@ -440,8 +441,9 @@ function makeReviewAgent(store: TaskStore, settings: Settings, taskId: string, o
|
||||
// The reviewer uses the project's validator/reviewer model lane (the same
|
||||
// one used elsewhere for review), falling back to the merger model only if
|
||||
// that lane resolves to nothing.
|
||||
const task = await store.getTask(taskId).catch(() => undefined);
|
||||
const validator = resolveValidatorSettingsModel(settings);
|
||||
const model = validator.provider && validator.modelId ? validator : resolveMergerSessionModel(settings);
|
||||
const model = validator.provider && validator.modelId ? validator : resolveMergerSessionModel(settings, undefined, task);
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: review merger retries share the dedicated merger fallback provider/model and thinking lane.
|
||||
const mergerFallbackModel = resolveMergerFallbackModel(settings);
|
||||
// FNXC:Settings-ThinkingLevel 2026-07-10-00:00: The review agent's model falls back
|
||||
@@ -449,7 +451,7 @@ function makeReviewAgent(store: TaskStore, settings: Settings, taskId: string, o
|
||||
// must follow the same lane it actually resolved a model from.
|
||||
const reviewThinkingLevel = validator.provider && validator.modelId
|
||||
? resolveValidatorThinkingLevel(undefined, settings)
|
||||
: resolveMergerThinkingLevel(settings);
|
||||
: resolveMergerThinkingLevel(settings, task?.mergerThinkingLevel);
|
||||
let captured = "";
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
@@ -481,7 +483,7 @@ function makeReviewAgent(store: TaskStore, settings: Settings, taskId: string, o
|
||||
defaultModelId: model.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, task?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: reviewThinkingLevel,
|
||||
runAuditor: audit,
|
||||
settings,
|
||||
|
||||
@@ -1110,7 +1110,8 @@ async function attemptInMergeVerificationFix(
|
||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||
const mergerTask = await store.getTask(taskId).catch(() => undefined);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig, mergerTask);
|
||||
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: merger retries use the dedicated project fallback lane before the shared global fallback pair.
|
||||
|
||||
@@ -1147,8 +1148,8 @@ Do not refactor, rename broadly, or make opportunistic improvements.
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: mergeRunContext?.runId ?? generateSyntheticRunId("merge", taskId),
|
||||
agentId: mergeRunContext?.agentId ?? "merger",
|
||||
@@ -2343,7 +2344,8 @@ async function runAiAgentForAutostashConflict(params: {
|
||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||
const mergerTask = await store.getTask(taskId).catch(() => undefined);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig, mergerTask);
|
||||
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: merger retries use the dedicated project fallback lane before the shared global fallback pair.
|
||||
|
||||
@@ -2395,8 +2397,8 @@ ${fileList}
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
agentId: "merger",
|
||||
@@ -2756,7 +2758,8 @@ async function runAiAgentForAutostashHardFail(params: {
|
||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||
const mergerTask = await store.getTask(taskId).catch(() => undefined);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig, mergerTask);
|
||||
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: merger retries use the dedicated project fallback lane before the shared global fallback pair.
|
||||
|
||||
@@ -2818,8 +2821,8 @@ ${fileList}
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
agentId: "merger",
|
||||
@@ -5818,7 +5821,8 @@ You are assisting with a paused \`git pull --rebase\`.
|
||||
});
|
||||
|
||||
throwIfAborted(options?.signal, taskId);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, options?.assignedAgentRuntimeConfig);
|
||||
const mergerTask = await store.getTask(taskId).catch(() => undefined);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, options?.assignedAgentRuntimeConfig, mergerTask);
|
||||
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: merger retries use the dedicated project fallback lane before the shared global fallback pair.
|
||||
|
||||
@@ -5838,8 +5842,8 @@ You are assisting with a paused \`git pull --rebase\`.
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
agentId: "merger",
|
||||
@@ -10783,7 +10787,8 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||
: null;
|
||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||
const mergerTask = await store.getTask(taskId).catch(() => undefined);
|
||||
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig, mergerTask);
|
||||
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: merger retries use the dedicated project fallback lane before the shared global fallback pair.
|
||||
|
||||
@@ -10808,8 +10813,8 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings, mergerTask?.mergerThinkingLevel),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
agentId: "merger",
|
||||
|
||||
@@ -100,7 +100,8 @@ export function makePrResponseAgentRunner(
|
||||
threads: Array<{ id: string }>;
|
||||
}) => Promise<PrAgentRunResult> {
|
||||
return async ({ prompt, systemPrompt, signal, threads }) => {
|
||||
const model = resolveMergerSessionModel(settings);
|
||||
const task = store ? await store.getTask(taskId).catch(() => undefined) : undefined;
|
||||
const model = resolveMergerSessionModel(settings, undefined, task);
|
||||
// FNXC:Settings-MergerModel 2026-07-16-00:00: PR-response retries use the merger-only fallback lane, retaining shared fallback inheritance when unset.
|
||||
const mergerFallbackModel = resolveMergerFallbackModel(settings);
|
||||
let captured = "";
|
||||
@@ -133,8 +134,8 @@ export function makePrResponseAgentRunner(
|
||||
defaultModelId: model.modelId,
|
||||
fallbackProvider: mergerFallbackModel.provider,
|
||||
fallbackModelId: mergerFallbackModel.modelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings, task?.mergerThinkingLevel),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings, task?.mergerThinkingLevel),
|
||||
settings,
|
||||
taskId,
|
||||
mcpServers,
|
||||
|
||||
Reference in New Issue
Block a user