From fe9fe88841ac10299911d204d200011d30ccd9fd Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 8 Aug 2026 19:13:14 -0700 Subject: [PATCH] FN-8847: remove retired ephemeral-agent setting Remove the obsolete ephemeral-agent compatibility setting across configuration, persistence, UI, and localization. - Remove the setting from schemas, scoped settings, persistence helpers, and General settings UI. - Update translations, settings documentation, and parity coverage for the retired option. - Adjust scheduler, executor, dashboard, and PostgreSQL tests for the unconditional behavior. - Add a patch changeset for @runfusion/fusion. Files changed: .changeset/fn-8847-remove-ephemeral-setting.md | 7 +++ docs/settings-reference.md | 3 +- .../postgres/settings-persistence.pg.test.ts | 14 ++++++ .../core/src/__tests__/settings-parity.test.ts | 20 ++++++-- packages/core/src/config/settings-schema.ts | 7 --- packages/core/src/task-store/settings-helpers.ts | 14 ++++-- packages/core/src/task-store/settings-ops.ts | 11 +++-- packages/core/src/types/settings/settings-scope.ts | 6 --- .../__tests__/SettingsModal.general.test.tsx | 41 ++++------------- .../settings/sections/GeneralSection.tsx | 17 ------- .../settings-default-descriptions.test.tsx | 6 --- .../src/__tests__/mission-start-routing.test.ts | 34 ++++++-------- ...executor-ephemeral-disabled-dispatch-gate.test.ts | 53 ++++++++-------------- .../__tests__/scheduler-ephemeral-toggle.test.ts | 30 +++++------- packages/i18n/locales/en/app.json | 2 - packages/i18n/locales/es/app.json | 2 - packages/i18n/locales/fr/app.json | 2 - packages/i18n/locales/ko/app.json | 2 - packages/i18n/locales/zh-CN/app.json | 2 - packages/i18n/locales/zh-TW/app.json | 2 - packages/i18n/src/resources.d.ts | 2 - 21 files changed, 105 insertions(+), 172 deletions(-) Fusion-Task-Id: FN-8847 Fusion-Task-Lineage: 8a067ec6-0ec7-4aef-a48e-604f821f35d2 Co-authored-by: Fusion (runfusion.ai) --- .../fn-8847-remove-ephemeral-setting.md | 7 +++ docs/settings-reference.md | 3 +- .../postgres/settings-persistence.pg.test.ts | 14 +++++ .../src/__tests__/settings-parity.test.ts | 20 +++++-- packages/core/src/config/settings-schema.ts | 7 --- .../core/src/task-store/settings-helpers.ts | 14 +++-- packages/core/src/task-store/settings-ops.ts | 11 ++-- .../core/src/types/settings/settings-scope.ts | 6 --- .../__tests__/SettingsModal.general.test.tsx | 41 ++++---------- .../settings/sections/GeneralSection.tsx | 17 ------ .../settings-default-descriptions.test.tsx | 6 --- .../__tests__/mission-start-routing.test.ts | 34 +++++------- ...r-ephemeral-disabled-dispatch-gate.test.ts | 53 +++++++------------ .../scheduler-ephemeral-toggle.test.ts | 30 ++++------- packages/i18n/locales/en/app.json | 2 - packages/i18n/locales/es/app.json | 2 - packages/i18n/locales/fr/app.json | 2 - packages/i18n/locales/ko/app.json | 2 - packages/i18n/locales/zh-CN/app.json | 2 - packages/i18n/locales/zh-TW/app.json | 2 - packages/i18n/src/resources.d.ts | 2 - 21 files changed, 105 insertions(+), 172 deletions(-) create mode 100644 .changeset/fn-8847-remove-ephemeral-setting.md diff --git a/.changeset/fn-8847-remove-ephemeral-setting.md b/.changeset/fn-8847-remove-ephemeral-setting.md new file mode 100644 index 0000000000..5c43f09fcf --- /dev/null +++ b/.changeset/fn-8847-remove-ephemeral-setting.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": major +--- + +summary: Remove the retired ephemeral-agent compatibility setting and control. +category: breaking +dev: Stale ephemeralAgentsEnabled values are discarded during settings reads and updates. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index a85e0a918d..ea46bebe78 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -623,7 +623,6 @@ Default notes: | `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). | | `verificationCommandTimeoutMs` | `number` | `undefined` | Optional project-scoped default timeout in milliseconds for executor `fn_run_verification` and configured deterministic test/build verification commands. When unset, `fn_run_verification` keeps its scope defaults (300s package, 900s workspace); when set to a positive value, it overrides both scope defaults while all verification still respects the 1800s hard cap. Set `0` or leave unset to use the legacy scope defaults. Marathon command shapes (`pnpm test`, `pnpm test:full`, `pnpm verify:workspace`, whole-package tests without file filters, and repeat loops) are soft-capped unless the agent explicitly passes `allowFullSuite: true`; opt-in full-suite runs still emit progress heartbeats and obey the hard cap. Project settings override global/default settings via the normal project settings precedence. | | `requirePlanApproval` | `boolean` | `false` | Require manual approval before planning → todo. | -| `ephemeralAgentsEnabled` | `boolean` | `true` | Persisted project compatibility input retained for existing configurations and clients. It cannot control scheduler admission or assignment, executor dispatch/re-entry, mission start, or workflow-stage principal routing. Classified workflow sessions always route through durable multi-role principals; operators configure workflow-session capacity on agents instead. | | `agentProvisioning` | `{ approvalMode?: "always" \| "trusted-only" \| "never"; trustedRoles?: string[]; trustedAgentIds?: string[]; alwaysApproveDelete?: boolean }` | `{}` | Approval policy for `fn_agent_create`/`fn_agent_delete` (`approvalMode` default `trusted-only`, delete approvals default on via `alwaysApproveDelete: true`). | | `sandboxProvisioning` | `{ approvalMode?: "always" \| "trusted-only" \| "never"; trustedRoles?: string[]; trustedAgentIds?: string[]; autoApproveBackendIds?: string[] }` | `{}` | Approval policy for sandbox host-bootstrap operations (backend install/pull/probe during `SandboxBackend.prepare()`). Default posture is strict: `approvalMode` resolves to `always`; `autoApproveBackendIds` defaults to `["native"]`. | | `completionDocumentationMode` | `"off" \| "changeset" \| "changelog"` | `"off"` | Controls triage prompt injection for release-note artifacts in future task specs. `"changeset"` requires `.changeset/*.md` workflow guidance; `"changelog"` requires updating an existing changelog file (without inventing a new one); `"off"` disables this automation. | @@ -1903,4 +1902,4 @@ Settings → Authentication can hold multiple named credential accounts for each ### Workflow principal limits -`runtimeConfig.maxWorkflowSessions` is an optional per-agent cap for durable workflow sessions. It is independent of heartbeat `maxConcurrentRuns`: enabling a built-in agent heartbeat neither consumes nor changes workflow-session capacity. The persisted `ephemeralAgentsEnabled` compatibility input defaults to `true`; it cannot control scheduler admission or assignment, executor dispatch/re-entry, mission start, or workflow-stage principal routing. +`runtimeConfig.maxWorkflowSessions` is an optional per-agent cap for durable workflow sessions. It is independent of heartbeat `maxConcurrentRuns`: enabling a built-in agent heartbeat neither consumes nor changes workflow-session capacity. Scheduler release, executor graph admission and re-entry, mission start, and workflow-stage routing are governed by durable workflow principals and their configured capacity. diff --git a/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts b/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts index 0951df958c..14c5bf36c4 100644 --- a/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts +++ b/packages/core/src/__tests__/postgres/settings-persistence.pg.test.ts @@ -56,6 +56,20 @@ pgTest("VAL-CROSS-004: Settings persistence (PostgreSQL)", () => { expect(settings.autoMerge).toBe(false); }); + it("discards retired ephemeral compatibility patches while preserving active project settings", async () => { + const store = h.store(); + await store.updateSettings({ + ephemeralAgentsEnabled: false, + ephemeralAgentTaskCreationPolicy: "deny", + taskPrefix: "ACTIVE", + } as never); + + const settings = await store.getSettings(); + expect(settings).not.toHaveProperty("ephemeralAgentsEnabled"); + expect(settings.ephemeralAgentTaskCreationPolicy).toBe("deny"); + expect(settings.taskPrefix).toBe("ACTIVE"); + }); + it("keeps the recommendation cap project-scoped when an untyped global patch includes it", async () => { const store = h.store(); await store.updateSettings({ maxRecommendationsPerTask: 7 }); diff --git a/packages/core/src/__tests__/settings-parity.test.ts b/packages/core/src/__tests__/settings-parity.test.ts index d9b96a9ada..a606338a78 100644 --- a/packages/core/src/__tests__/settings-parity.test.ts +++ b/packages/core/src/__tests__/settings-parity.test.ts @@ -288,12 +288,22 @@ describe("settings key parity", () => { expect(isGlobalSettingsKey("executorAllowSiblingBranchRename")).toBe(false); }); - it("defaults the routing-inert ephemeral compatibility input and keeps it project-scoped", () => { - expect(DEFAULT_PROJECT_SETTINGS.ephemeralAgentsEnabled).toBe(true); - expect(isProjectSettingsKey("ephemeralAgentsEnabled")).toBe(true); + it("removes the retired ephemeral compatibility input from settings and canonicalizes stale values", () => { + expect(DEFAULT_PROJECT_SETTINGS).not.toHaveProperty("ephemeralAgentsEnabled"); + expect(isProjectSettingsKey("ephemeralAgentsEnabled")).toBe(false); expect(isGlobalSettingsKey("ephemeralAgentsEnabled")).toBe(false); - expect(canonicalizeSettings({ ephemeralAgentsEnabled: false } as import("../types.js").Settings)) - .toMatchObject({ ephemeralAgentsEnabled: false }); + + for (const retiredValue of [true, false]) { + const persisted = { ephemeralAgentsEnabled: retiredValue, taskPrefix: "SURVIVES" } as import("../types.js").Settings; + const canonical = canonicalizeSettings(persisted); + expect(canonical).not.toHaveProperty("ephemeralAgentsEnabled"); + expect(canonical.taskPrefix).toBe("SURVIVES"); + expect(persisted).toHaveProperty("ephemeralAgentsEnabled", retiredValue); + } + + const absent = { taskPrefix: "UNCHANGED" } as import("../types.js").Settings; + expect(canonicalizeSettings(absent)).not.toHaveProperty("ephemeralAgentsEnabled"); + expect(canonicalizeSettings(absent).taskPrefix).toBe("UNCHANGED"); }); it("defaults ephemeralAgentsCanCreateTasks to true and keeps it project-scoped", () => { diff --git a/packages/core/src/config/settings-schema.ts b/packages/core/src/config/settings-schema.ts index b35e560ff1..4f37d0bf96 100644 --- a/packages/core/src/config/settings-schema.ts +++ b/packages/core/src/config/settings-schema.ts @@ -632,13 +632,6 @@ export const DEFAULT_PROJECT_SETTINGS = { // coverage. Falls back to package/explicit command when no tests resolve. scopeVerificationToChangedFiles: true, /* - FNXC:WorkflowAgentRouting 2026-08-07-08:45: - Keep the legacy project setting defaulted and persistable for existing clients and - configuration records. Workflow-stage routing must ignore its value; durable role - principals own scheduler, executor, and mission-stage authority. - */ - ephemeralAgentsEnabled: true, - /* FNXC:EphemeralAgentTaskCreation 2026-07-01-00:00: Default-on so ephemeral task-worker agents keep the ability to open follow-up tasks via fn_task_create. Operators who want to confine task creation to humans/permanent agents flip this off. */ diff --git a/packages/core/src/task-store/settings-helpers.ts b/packages/core/src/task-store/settings-helpers.ts index 436d5b0579..ac3d6e739f 100644 --- a/packages/core/src/task-store/settings-helpers.ts +++ b/packages/core/src/task-store/settings-helpers.ts @@ -15,12 +15,16 @@ import { validateWorktrunkSettings } from "../config/worktrunk-settings.js"; */ export function canonicalizeSettings(settings: Settings): Settings { /* - FNXC:WorkflowAgentRouting 2026-08-07-08:45: - Strip only the obsolete globalMaxConcurrent key. ephemeralAgentsEnabled remains a persisted - compatibility input; scheduler, executor, and mission routing deliberately ignore its value. + FNXC:WorkflowAgentRouting 2026-08-09-01:04: + FN-8847 removes the retired ephemeralAgentsEnabled compatibility input. Strip it with the + earlier globalMaxConcurrent field at every read boundary so stale configuration payloads cannot + reappear or affect durable workflow-principal routing. */ - const { globalMaxConcurrent, ...rest } = settings as Settings & { globalMaxConcurrent?: number }; - const base = globalMaxConcurrent !== undefined ? (rest as Settings) : settings; + const { globalMaxConcurrent, ephemeralAgentsEnabled: _ephemeralAgentsEnabled, ...rest } = settings as Settings & { + globalMaxConcurrent?: number; + ephemeralAgentsEnabled?: boolean; + }; + const base = globalMaxConcurrent !== undefined || _ephemeralAgentsEnabled !== undefined ? (rest as Settings) : settings; const canonicalWorktrunk = (() => { try { diff --git a/packages/core/src/task-store/settings-ops.ts b/packages/core/src/task-store/settings-ops.ts index fcc1050fed..b686d7138d 100644 --- a/packages/core/src/task-store/settings-ops.ts +++ b/packages/core/src/task-store/settings-ops.ts @@ -104,14 +104,15 @@ export async function updateSettingsImpl(store: TaskStore, patch: Partial = {}; for (const [key, value] of Object.entries(guardedPatch)) { - if (!isGlobalOnlySettingsKey(key)) { + if (!isGlobalOnlySettingsKey(key) && key !== "ephemeralAgentsEnabled") { (projectPatch as Record)[key] = value; } } diff --git a/packages/core/src/types/settings/settings-scope.ts b/packages/core/src/types/settings/settings-scope.ts index 3a69b39748..f642663426 100644 --- a/packages/core/src/types/settings/settings-scope.ts +++ b/packages/core/src/types/settings/settings-scope.ts @@ -1751,12 +1751,6 @@ export interface ProjectSettings { * FN-7557: default is now "auto-approve-all" (previously deferred to workflow via "workflow"). Unset/new projects bypass the manual awaiting-approval gate by default; projects with an explicit stored value are unaffected. */ planApprovalMode?: "workflow" | "auto-approve-all" | "require-all"; - /** - * FNXC:WorkflowAgentRouting 2026-08-07-08:57: - * Retain this legacy input for persisted settings and client compatibility. Workflow stages - * always route through durable multi-role principals; this flag cannot affect that routing. - */ - ephemeralAgentsEnabled?: boolean; /* FNXC:EphemeralAgentTaskCreation 2026-07-30-12:00: The three-state policy routes ephemeral-worker follow-ups to allow, operator validation, or deny. diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx index f5b6a023dd..010fb3a0ad 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx @@ -1402,41 +1402,20 @@ describe("SettingsModal", () => { expect(screen.getByRole("option", { name: "Require changelog update (existing changelog)" })).toBeInTheDocument(); }); - it("renders the enabled default in routing-inert ephemeral agent compatibility help", async () => { + it("omits the retired ephemeral compatibility controls while preserving follow-up policy", async () => { renderModal({ initialSection: "general" }); await waitForSettingsModalReady(); - const helpTrigger = screen.getByTestId("settings-help-ephemeralAgentsEnabled"); - const helpId = helpTrigger.getAttribute("aria-describedby"); - expect(helpId).toBeTruthy(); + expect(screen.queryByLabelText("Use ephemeral task-worker agents")).not.toBeInTheDocument(); + expect(screen.queryByTestId("settings-help-ephemeralAgentsEnabled")).not.toBeInTheDocument(); + expect(document.getElementById("ephemeralAgentsEnabled")).toBeNull(); - const helpNote = document.getElementById(helpId!); - expect(helpNote).toHaveAttribute("role", "note"); - expect(helpNote).toHaveTextContent(/default: enabled/i); - expect(helpNote).toHaveTextContent(/does not affect scheduler assignment or admission, executor workflow dispatch or re-entry, mission start, or workflow-stage principal routing/i); - }); - - it("persists the routing-inert ephemeral agent compatibility input", async () => { - renderModal({ initialSection: "general" }); - await waitForSettingsModalReady(); - - const toggle = screen.getByLabelText("Use ephemeral task-worker agents") as HTMLInputElement; - expect(toggle.checked).toBe(true); - await settingsModalUser.click(toggle); - - await waitFor(() => expect(mockUpdateSettings).toHaveBeenCalled()); - expect(mockUpdateSettings.mock.calls[0]?.[0]).toMatchObject({ ephemeralAgentsEnabled: false }); - }); - - it("defaults the ephemeral agent compatibility input when an upgraded record omits it", async () => { - const { ephemeralAgentsEnabled: _omitted, ...upgradeSettings } = defaultSettings; - mockFetchSettings.mockResolvedValueOnce(upgradeSettings); - mockFetchSettingsByScope.mockResolvedValueOnce({ global: defaultSettings, project: {} }); - - renderModal({ initialSection: "general" }); - await waitForSettingsModalReady(); - - expect((screen.getByLabelText("Use ephemeral task-worker agents") as HTMLInputElement).checked).toBe(true); + const policy = screen.getByLabelText("Ephemeral agent follow-up tasks"); + expect(policy).toBeInTheDocument(); + await settingsModalUser.selectOptions(policy, "deny"); + await waitFor(() => expect(mockUpdateSettings.mock.calls[0]?.[0]).toMatchObject({ + ephemeralAgentTaskCreationPolicy: "deny", + })); }); it("reports Quick Chat launcher changes immediately before save", async () => { diff --git a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx index 9f73fc7bd4..19a51a3df8 100644 --- a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx @@ -298,23 +298,6 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError {!isKnownSelectableWorkflow(refinementTaskWorkflowValue) && ()} -
-
- {/* - FNXC:EphemeralAgentCompatibility 2026-08-08-06:12: - FN-8831 requires this visible compatibility input to state its canonical enabled default in the shared help tip. It remains routing-inert: the saved value never controls scheduler admission, executor dispatch or re-entry, mission start, or workflow-stage principal routing. - */} - - {t("settings.general.ephemeralAgentsCompatibilityHint", " Retained for configuration compatibility. Default: enabled. This setting does not affect scheduler assignment or admission, executor workflow dispatch or re-entry, mission start, or workflow-stage principal routing. ")} -
-
- {/* - FNXC:WorkflowAgentRouting 2026-08-07-08:45: - Retain this project setting's UI round trip for operators with existing configuration. - Its value is intentionally routing-inert: durable workflow principals decide all stages. - */} {/* FNXC:EphemeralAgentTaskCreation 2026-07-30-12:00: Operators choose free creation, an operator-mailbox proposal, or denial for ephemeral worker follow-ups. diff --git a/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx b/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx index e7f15e2f50..af5f3763d1 100644 --- a/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx +++ b/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx @@ -252,12 +252,6 @@ const SETTING_DESCRIPTION_KEYS: Record = { worktreeRebaseLocalBase: "worktrees.inAdditionToTheRemoteRebaseAboveAlso", worktrunk: "worktrees.disabledByDefaultOptInWhenEnabledFusion", // GeneralSection (project) - /* - * FNXC:EphemeralAgentCompatibility 2026-08-08-06:12: - * FN-8831 keeps the visible compatibility checkbox in the surfaced-description inventory. - * Its English help must state the enabled schema default while preserving that it is routing-inert. - */ - ephemeralAgentsEnabled: "general.ephemeralAgentsCompatibilityHint", allowAbsoluteFileBrowserPaths: "general.allowAbsoluteFileBrowserPathsHint", capacityRiskBannerEnabled: "general.warnOnTheBoardWhenTodoWorkExceeds", capacityRiskTodoThreshold: "general.bannerFiresWhenTodoCountIsStrictlyGreater", diff --git a/packages/dashboard/src/__tests__/mission-start-routing.test.ts b/packages/dashboard/src/__tests__/mission-start-routing.test.ts index d35f406c7b..f08f08d612 100644 --- a/packages/dashboard/src/__tests__/mission-start-routing.test.ts +++ b/packages/dashboard/src/__tests__/mission-start-routing.test.ts @@ -5,8 +5,6 @@ import type { TaskStore } from "@fusion/core"; import { createMissionRouter } from "../mission-routes.js"; import { request } from "../test-request.js"; -type CompatibilitySetting = boolean | undefined; - type Mission = { id: string; status: "planning" | "active" | "blocked"; @@ -20,7 +18,7 @@ type Slice = { status: "pending" | "active"; }; -function createFixture(ephemeralAgentsEnabled: CompatibilitySetting) { +function createFixture() { const mission: Mission = { id: "M-START-1", status: "planning", @@ -28,7 +26,7 @@ function createFixture(ephemeralAgentsEnabled: CompatibilitySetting) { autopilotEnabled: false, }; const slice: Slice = { id: "SL-START-1", missionId: mission.id, status: "pending" }; - const getSettings = vi.fn(async () => ({ ephemeralAgentsEnabled })); + const getSettings = vi.fn(async () => ({})); const missionStore = { getMission: vi.fn(async (id: string) => id === mission.id ? mission : undefined), findNextPendingSlice: vi.fn(async (id: string) => id === mission.id && slice.status === "pending" ? slice : undefined), @@ -36,8 +34,8 @@ function createFixture(ephemeralAgentsEnabled: CompatibilitySetting) { Object.assign(mission, patch); return mission; }), - activateSlice: vi.fn(async (id: string) => { - if (id === slice.id) slice.status = "active"; + tryActivateNextPendingSlice: vi.fn(async (id: string) => { + if (id === mission.id && slice.status === "pending") slice.status = "active"; return slice; }), getMissionWithHierarchy: vi.fn(async (id: string) => id === mission.id ? { ...mission, milestones: [{ slices: [{ ...slice, features: [] }] }] } : undefined), @@ -56,16 +54,13 @@ function createFixture(ephemeralAgentsEnabled: CompatibilitySetting) { } /* -FNXC:MissionRouting 2026-08-07-08:36: -`ephemeralAgentsEnabled` is a retired client-compatibility input, not mission admission policy. -Mission start must activate a valid planning mission and its pending slice without reading settings or -preflighting legacy executor inventory; durable workflow principals are resolved only at workflow execution. +FNXC:MissionRouting 2026-08-09-01:04: +FN-8847 removes the retired compatibility input. Mission start activates a valid planning mission +and pending slice without settings reads; durable principals resolve only at workflow execution. */ describe("mission start routing", () => { - it.each([undefined, true, false])( - "activates a planning mission independently of compatibility input %s", - async (ephemeralAgentsEnabled) => { - const { app, getSettings, mission, missionStore, slice } = createFixture(ephemeralAgentsEnabled); + it("activates a planning mission without reading project settings", async () => { + const { app, getSettings, mission, missionStore, slice } = createFixture(); const response = await request(app, "POST", `/api/missions/${mission.id}/start`); @@ -75,24 +70,23 @@ describe("mission start routing", () => { expect(mission.autopilotEnabled).toBe(true); expect(slice.status).toBe("active"); expect(missionStore.updateMission).toHaveBeenCalledOnce(); - expect(missionStore.activateSlice).toHaveBeenCalledWith(slice.id); + expect(missionStore.tryActivateNextPendingSlice).toHaveBeenCalledWith(mission.id); expect(getSettings).not.toHaveBeenCalled(); - }, - ); + }); it("rejects a non-planning mission without activating its slice", async () => { - const { app, mission, missionStore, slice } = createFixture(false); + const { app, mission, missionStore, slice } = createFixture(); mission.status = "active"; const response = await request(app, "POST", `/api/missions/${mission.id}/start`); expect(response.status).toBe(409); expect(slice.status).toBe("pending"); - expect(missionStore.activateSlice).not.toHaveBeenCalled(); + expect(missionStore.tryActivateNextPendingSlice).not.toHaveBeenCalled(); }); it("rejects a planning mission with no pending slice", async () => { - const { app, mission, missionStore, slice } = createFixture(false); + const { app, mission, missionStore, slice } = createFixture(); slice.status = "active"; const response = await request(app, "POST", `/api/missions/${mission.id}/start`); diff --git a/packages/engine/src/__tests__/executor-ephemeral-disabled-dispatch-gate.test.ts b/packages/engine/src/__tests__/executor-ephemeral-disabled-dispatch-gate.test.ts index 771355c68f..fbc3b7d0f3 100644 --- a/packages/engine/src/__tests__/executor-ephemeral-disabled-dispatch-gate.test.ts +++ b/packages/engine/src/__tests__/executor-ephemeral-disabled-dispatch-gate.test.ts @@ -33,22 +33,20 @@ function task(overrides: Partial = {}): TaskDetail { } as TaskDetail; } -function settings(ephemeralAgentsEnabled: boolean | undefined) { +function settings() { return { autoMerge: true, maxAutoMergeRetries: 3, maxConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 15_000, - ephemeralAgentsEnabled, }; } /* -FNXC:WorkflowAgentRouting 2026-08-07-09:11: -A non-scheduler TaskExecutor.execute() entry must run the real graph admission hook regardless -of the persisted compatibility value. The graph owns durable principal acquisition, unavailable -principal holds, and capacity fencing; outer dispatch must not rebound or queue work under this setting. +FNXC:WorkflowAgentRouting 2026-08-09-01:04: +FN-8847 removes the retired compatibility input. Every TaskExecutor.execute() entry must use graph +admission for durable principal acquisition, unavailable-principal holds, and capacity fencing. */ const graphDefinition = { id: "WF-fn-8821-principal", @@ -78,7 +76,6 @@ function durableExecutorAgent(overrides: Record = {}) { } function createProductionGraphHarness( - ephemeralAgentsEnabled: boolean | undefined, agents: unknown[] = [durableExecutorAgent()], taskOverrides: Partial = {}, semaphore?: AgentSemaphore, @@ -87,7 +84,7 @@ function createProductionGraphHarness( const live = task(taskOverrides); store.getTask.mockResolvedValue(live); store.getRootDir = vi.fn(() => "/tmp/fn-8821-project"); - store.getSettings.mockResolvedValue(settings(ephemeralAgentsEnabled)); + store.getSettings.mockResolvedValue(settings()); store.getTaskWorkflowSelectionAsync.mockResolvedValue({ workflowId: graphDefinition.id, stepIds: [] }); store.getTaskWorkflowSelection.mockReturnValue({ workflowId: graphDefinition.id, stepIds: [] }); store.getWorkflowDefinition = vi.fn(async () => graphDefinition); @@ -102,17 +99,15 @@ function createProductionGraphHarness( return { store, live, executor, agentStore, acquire, release }; } -describe("executor compatibility setting is routing-inert", () => { +describe("executor routes workflow stages through durable principals", () => { afterEach(() => { clearPreHeldExecutorSlotsForTests(); (TaskExecutor as unknown as { processWideGraphRouting: Set }).processWideGraphRouting.clear(); }); - it.each([undefined, true, false])( - "runs real graph principal admission for unassigned direct re-entry with compatibility input %s", - async (ephemeralAgentsEnabled) => { + it("runs real graph principal admission for unassigned direct re-entry", async () => { resetExecutorMocks(); - const { store, live, executor, agentStore, acquire, release } = createProductionGraphHarness(ephemeralAgentsEnabled); + const { store, live, executor, agentStore, acquire, release } = createProductionGraphHarness(); await executor.execute(live); @@ -140,17 +135,13 @@ describe("executor compatibility setting is routing-inert", () => { expect.anything(), expect.anything(), ); - }, - ); + }); - it.each([undefined, true, false])( - "preserves assigned workflow principals through scheduler-held graph dispatch for compatibility input %s", - async (ephemeralAgentsEnabled) => { + it("preserves assigned workflow principals through scheduler-held graph dispatch", async () => { resetExecutorMocks(); const semaphore = new AgentSemaphore(1); expect(semaphore.tryAcquire()).toBe(true); const { store, live, executor, acquire, release } = createProductionGraphHarness( - ephemeralAgentsEnabled, [durableExecutorAgent()], { assignedAgentId: "workflow-executor" }, semaphore, @@ -171,13 +162,11 @@ describe("executor compatibility setting is routing-inert", () => { expect(semaphore.activeCount).toBe(0); expect(store.moveTask).not.toHaveBeenCalled(); expect(store.transitionQueuedEpisode).not.toHaveBeenCalled(); - }, - ); + }); - it("keeps an explicit-false direct re-entry behind the real unmet-dependency gate", async () => { + it("keeps direct re-entry behind the real unmet-dependency gate", async () => { resetExecutorMocks(); const { store, live, executor, agentStore, acquire } = createProductionGraphHarness( - false, [durableExecutorAgent()], { dependencies: ["FN-8821-PARENT"] }, ); @@ -199,11 +188,9 @@ describe("executor compatibility setting is routing-inert", () => { expect(store.upsertWorkflowWorkItem).not.toHaveBeenCalled(); }); - it.each([undefined, true, false])( - "holds unavailable workflow principals through their graph owner for compatibility input %s", - async (ephemeralAgentsEnabled) => { + it("holds unavailable workflow principals through their graph owner", async () => { resetExecutorMocks(); - const { store, live, executor, agentStore, acquire } = createProductionGraphHarness(ephemeralAgentsEnabled, []); + const { store, live, executor, agentStore, acquire } = createProductionGraphHarness([]); await executor.execute(live); @@ -217,14 +204,11 @@ describe("executor compatibility setting is routing-inert", () => { })); expect(store.moveTask).not.toHaveBeenCalled(); expect(store.transitionQueuedEpisode).not.toHaveBeenCalled(); - }, - ); + }); - it.each([undefined, true, false])( - "holds saturated workflow principals through graph capacity for compatibility input %s", - async (ephemeralAgentsEnabled) => { + it("holds saturated workflow principals through graph capacity", async () => { resetExecutorMocks(); - const { store, live, executor, acquire } = createProductionGraphHarness(ephemeralAgentsEnabled); + const { store, live, executor, acquire } = createProductionGraphHarness(); acquire.mockResolvedValue({ status: "held", reason: "project-capacity" }); await executor.execute(live); @@ -238,6 +222,5 @@ describe("executor compatibility setting is routing-inert", () => { })); expect(store.moveTask).not.toHaveBeenCalled(); expect(store.transitionQueuedEpisode).not.toHaveBeenCalled(); - }, - ); + }); }); diff --git a/packages/engine/src/__tests__/scheduler-ephemeral-toggle.test.ts b/packages/engine/src/__tests__/scheduler-ephemeral-toggle.test.ts index d0b9bc6f96..93841dfeed 100644 --- a/packages/engine/src/__tests__/scheduler-ephemeral-toggle.test.ts +++ b/packages/engine/src/__tests__/scheduler-ephemeral-toggle.test.ts @@ -39,7 +39,7 @@ function task(overrides: Partial = {}): Task { } as Task; } -function storeWith(ready: Task, ephemeralAgentsEnabled: boolean | undefined): TaskStore { +function storeWith(ready: Task): TaskStore { const updateTask = vi.fn(async (_id: string, patch: Partial) => Object.assign(ready, patch)); return { listTasks: vi.fn(async () => [ready]), @@ -47,7 +47,6 @@ function storeWith(ready: Task, ephemeralAgentsEnabled: boolean | undefined): Ta getSettings: vi.fn(async () => ({ maxConcurrent: 2, maxWorktrees: 4, - ephemeralAgentsEnabled, })), updateSettings: vi.fn(async () => undefined), updateTask, @@ -74,23 +73,20 @@ function storeWith(ready: Task, ephemeralAgentsEnabled: boolean | undefined): Ta } /* -FNXC:WorkflowScheduling 2026-08-07-09:01: -`ephemeralAgentsEnabled` remains an accepted persisted compatibility input, but scheduler release -must not inspect it to assign, queue, or reject workflow work. Principal selection and capacity -belong to graph admission after this production scheduler handoff. +FNXC:WorkflowScheduling 2026-08-09-01:04: +FN-8847 removes the retired compatibility toggle. Scheduler release must hand workflow tasks to +graph admission without legacy assignment or queueing; durable principals resolve that authority later. */ -describe("scheduler compatibility setting is routing-inert", () => { +describe("scheduler releases workflow tasks for durable principal routing", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(existsSync).mockReturnValue(true); vi.mocked(readFile).mockResolvedValue("# Task\nBody"); }); - it.each([undefined, true, false])( - "releases an unassigned workflow task with compatibility input %s without legacy assignment or queueing", - async (ephemeralAgentsEnabled) => { + it("releases an unassigned workflow task without legacy assignment or queueing", async () => { const ready = task(); - const store = storeWith(ready, ephemeralAgentsEnabled); + const store = storeWith(ready); const onSchedule = vi.fn(); const scheduler = new Scheduler(store, { onSchedule }); (scheduler as unknown as { running: boolean }).running = true; @@ -102,14 +98,11 @@ describe("scheduler compatibility setting is routing-inert", () => { expect(store.updateTask).not.toHaveBeenCalledWith(ready.id, expect.objectContaining({ assignedAgentId: expect.any(String) })); expect(store.updateTask).not.toHaveBeenCalledWith(ready.id, expect.objectContaining({ status: "queued" })); expect(store.transitionQueuedEpisode).not.toHaveBeenCalled(); - }, - ); + }); - it.each([undefined, true, false])( - "preserves an assigned task's normal release for compatibility input %s", - async (ephemeralAgentsEnabled) => { + it("preserves an assigned task's normal release", async () => { const ready = task({ id: "FN-8821-SCHEDULER-ASSIGNED", assignedAgentId: "durable-owner" }); - const store = storeWith(ready, ephemeralAgentsEnabled); + const store = storeWith(ready); const onSchedule = vi.fn(); const scheduler = new Scheduler(store, { onSchedule }); (scheduler as unknown as { running: boolean }).running = true; @@ -120,6 +113,5 @@ describe("scheduler compatibility setting is routing-inert", () => { expect(ready.assignedAgentId).toBe("durable-owner"); expect(onSchedule).toHaveBeenCalledWith(expect.objectContaining({ id: ready.id, column: "in-progress" })); expect(store.transitionQueuedEpisode).not.toHaveBeenCalled(); - }, - ); + }); }); diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 47f77b4cae..6bdd616952 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -5967,8 +5967,6 @@ "updating": "Updating…", "upperBoundOnMessagesFetchedFromTheRoom": "Upper bound on messages fetched from the room store for compaction consideration. Default: 200.", "upToDate": "You're up to date ✓", - "useEphemeralTaskWorkerAgents": " Use ephemeral task-worker agents ", - "ephemeralAgentsCompatibilityHint": "Retained for configuration compatibility. Default: enabled. This setting does not affect scheduler assignment or admission, executor workflow dispatch or re-entry, mission start, or workflow-stage principal routing.", "warnOnTheBoardWhenTodoWorkExceeds": "Warn on the board when todo work exceeds the threshold and no idle agents are available. Default: disabled.", "whenEnabledDefaultFusionSpawnsShortLived": " When enabled (default), Fusion spawns short-lived ", "whenEnabledFusionChecksOpenAndClosedIssues": " When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue. Default: enabled. ", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 66c7f35e16..f58af10887 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -5930,7 +5930,6 @@ "updating": "", "upperBoundOnMessagesFetchedFromTheRoom": "", "upToDate": "Estás actualizado ✓", - "useEphemeralTaskWorkerAgents": "", "warnOnTheBoardWhenTodoWorkExceeds": "", "whenEnabledDefaultFusionSpawnsShortLived": "", "whenEnabledFusionChecksOpenAndClosedIssues": "", @@ -6002,7 +6001,6 @@ "taskCreateWorkflowHelp": "", "refinementTaskWorkflow": "", "refinementTaskWorkflowHelp": "", - "ephemeralAgentsCompatibilityHint": "", "maxRecommendationsPerTask": "", "maxRecommendationsPerTaskHelp": "" }, diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 2fe4bea19b..8de40d4edf 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -5930,7 +5930,6 @@ "updating": "", "upperBoundOnMessagesFetchedFromTheRoom": "", "upToDate": "Vous êtes à jour ✓", - "useEphemeralTaskWorkerAgents": "", "warnOnTheBoardWhenTodoWorkExceeds": "", "whenEnabledDefaultFusionSpawnsShortLived": "", "whenEnabledFusionChecksOpenAndClosedIssues": "", @@ -6002,7 +6001,6 @@ "taskCreateWorkflowHelp": "", "refinementTaskWorkflow": "", "refinementTaskWorkflowHelp": "", - "ephemeralAgentsCompatibilityHint": "", "maxRecommendationsPerTask": "", "maxRecommendationsPerTaskHelp": "" }, diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index 64a9c8ad38..6c0115df9a 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -5930,7 +5930,6 @@ "updating": "", "upperBoundOnMessagesFetchedFromTheRoom": "", "upToDate": "최신 버전입니다 ✓", - "useEphemeralTaskWorkerAgents": "", "warnOnTheBoardWhenTodoWorkExceeds": "", "whenEnabledDefaultFusionSpawnsShortLived": "", "whenEnabledFusionChecksOpenAndClosedIssues": "", @@ -6002,7 +6001,6 @@ "taskCreateWorkflowHelp": "", "refinementTaskWorkflow": "", "refinementTaskWorkflowHelp": "", - "ephemeralAgentsCompatibilityHint": "", "maxRecommendationsPerTask": "", "maxRecommendationsPerTaskHelp": "" }, diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index c43bd63543..9a0e7be5fc 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -5930,7 +5930,6 @@ "updating": "", "upperBoundOnMessagesFetchedFromTheRoom": "", "upToDate": "您已是最新版本 ✓", - "useEphemeralTaskWorkerAgents": "", "warnOnTheBoardWhenTodoWorkExceeds": "", "whenEnabledDefaultFusionSpawnsShortLived": "", "whenEnabledFusionChecksOpenAndClosedIssues": "", @@ -6002,7 +6001,6 @@ "taskCreateWorkflowHelp": "", "refinementTaskWorkflow": "", "refinementTaskWorkflowHelp": "", - "ephemeralAgentsCompatibilityHint": "", "maxRecommendationsPerTask": "", "maxRecommendationsPerTaskHelp": "" }, diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index cdebefb54e..5ffd17cbf2 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -5930,7 +5930,6 @@ "updating": "", "upperBoundOnMessagesFetchedFromTheRoom": "", "upToDate": "您已是最新版本 ✓", - "useEphemeralTaskWorkerAgents": "", "warnOnTheBoardWhenTodoWorkExceeds": "", "whenEnabledDefaultFusionSpawnsShortLived": "", "whenEnabledFusionChecksOpenAndClosedIssues": "", @@ -6002,7 +6001,6 @@ "taskCreateWorkflowHelp": "", "refinementTaskWorkflow": "", "refinementTaskWorkflowHelp": "", - "ephemeralAgentsCompatibilityHint": "", "maxRecommendationsPerTask": "", "maxRecommendationsPerTaskHelp": "" }, diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index 92d66b8f10..86f4bb82d0 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -5954,7 +5954,6 @@ export default interface Resources { "ephemeralAgentTaskCreationPolicyDeny": "Deny", "ephemeralAgentTaskCreationPolicyHint": "No default — unset policy falls back to Allow. Upon validation sends a proposal to your mailbox for one-click approval; Deny rejects follow-up task creation.", "ephemeralAgentTaskCreationPolicyUponValidation": "Upon validation", - "ephemeralAgentsCompatibilityHint": "Retained for configuration compatibility. Default: enabled. This setting does not affect scheduler assignment or admission, executor workflow dispatch or re-entry, mission start, or workflow-stage principal routing.", "fN": "FN", "followDashboardLanguage": "Follow dashboard language", "general": "General", @@ -6047,7 +6046,6 @@ export default interface Resources { "updateSuccessToast": "Update installed. Restart Fusion to apply it.", "updating": "Updating…", "upperBoundOnMessagesFetchedFromTheRoom": "Upper bound on messages fetched from the room store for compaction consideration. Default: 200.", - "useEphemeralTaskWorkerAgents": " Use ephemeral task-worker agents ", "warnOnTheBoardWhenTodoWorkExceeds": "Warn on the board when todo work exceeds the threshold and no idle agents are available. Default: disabled.", "whenEnabledDefaultFusionSpawnsShortLived": " When enabled (default), Fusion spawns short-lived ", "whenEnabledFusionChecksOpenAndClosedIssues": " When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue. Default: enabled. ",