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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-08 19:13:14 -07:00
parent b181f20859
commit fe9fe88841
21 changed files with 105 additions and 172 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 });

View File

@@ -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", () => {

View File

@@ -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.
*/

View File

@@ -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 {

View File

@@ -104,14 +104,15 @@ export async function updateSettingsImpl(store: TaskStore, patch: Partial<Settin
})()
: patch;
/*
FNXC:WorkflowAgentRouting 2026-08-07-08:45:
Preserve ephemeralAgentsEnabled in project updates for configuration compatibility. Its
routing-inert behavior is enforced exclusively by scheduler, executor, and mission consumers.
FNXC:WorkflowAgentRouting 2026-08-09-01:04:
FN-8847 rejects the retired ephemeralAgentsEnabled client patch before the configuration
revision transaction. Canonicalization also removes stale stored copies, while active
ephemeral task-creation policy fields keep their normal project-setting behavior.
*/
// Filter out global-only fields — they should go through updateGlobalSettings().
// Filter out global-only and retired project fields before writing a configuration revision.
const projectPatch: Partial<Settings> = {};
for (const [key, value] of Object.entries(guardedPatch)) {
if (!isGlobalOnlySettingsKey(key)) {
if (!isGlobalOnlySettingsKey(key) && key !== "ephemeralAgentsEnabled") {
(projectPatch as Record<string, unknown>)[key] = value;
}
}

View File

@@ -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.

View File

@@ -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 () => {

View File

@@ -298,23 +298,6 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
{!isKnownSelectableWorkflow(refinementTaskWorkflowValue) && (<option value={refinementTaskWorkflowValue}>{refinementTaskWorkflowValue}</option>)}
</select>
</div>
<div className="form-group">
<div className="settings-field-label-row">
{/*
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.
*/}
<label htmlFor="ephemeralAgentsEnabled" className="checkbox-label">
<input id="ephemeralAgentsEnabled" type="checkbox" checked={form.ephemeralAgentsEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, ephemeralAgentsEnabled: e.target.checked }))}/>{t("settings.general.useEphemeralTaskWorkerAgents", " Use ephemeral task-worker agents ")}
</label>
<SettingsHelpTip settingKey="ephemeralAgentsEnabled">{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. ")}</SettingsHelpTip>
</div>
</div>
{/*
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.

View File

@@ -252,12 +252,6 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
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",

View File

@@ -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<CompatibilitySetting>([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`);

View File

@@ -33,22 +33,20 @@ function task(overrides: Partial<TaskDetail> = {}): 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<string, unknown> = {}) {
}
function createProductionGraphHarness(
ephemeralAgentsEnabled: boolean | undefined,
agents: unknown[] = [durableExecutorAgent()],
taskOverrides: Partial<TaskDetail> = {},
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<string> }).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();
},
);
});
});

View File

@@ -39,7 +39,7 @@ function task(overrides: Partial<Task> = {}): 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<Task>) => 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();
},
);
});
});

View File

@@ -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. ",

View File

@@ -5930,7 +5930,6 @@
"updating": "",
"upperBoundOnMessagesFetchedFromTheRoom": "",
"upToDate": "Estás actualizado ✓",
"useEphemeralTaskWorkerAgents": "",
"warnOnTheBoardWhenTodoWorkExceeds": "",
"whenEnabledDefaultFusionSpawnsShortLived": "",
"whenEnabledFusionChecksOpenAndClosedIssues": "",
@@ -6002,7 +6001,6 @@
"taskCreateWorkflowHelp": "",
"refinementTaskWorkflow": "",
"refinementTaskWorkflowHelp": "",
"ephemeralAgentsCompatibilityHint": "",
"maxRecommendationsPerTask": "",
"maxRecommendationsPerTaskHelp": ""
},

View File

@@ -5930,7 +5930,6 @@
"updating": "",
"upperBoundOnMessagesFetchedFromTheRoom": "",
"upToDate": "Vous êtes à jour ✓",
"useEphemeralTaskWorkerAgents": "",
"warnOnTheBoardWhenTodoWorkExceeds": "",
"whenEnabledDefaultFusionSpawnsShortLived": "",
"whenEnabledFusionChecksOpenAndClosedIssues": "",
@@ -6002,7 +6001,6 @@
"taskCreateWorkflowHelp": "",
"refinementTaskWorkflow": "",
"refinementTaskWorkflowHelp": "",
"ephemeralAgentsCompatibilityHint": "",
"maxRecommendationsPerTask": "",
"maxRecommendationsPerTaskHelp": ""
},

View File

@@ -5930,7 +5930,6 @@
"updating": "",
"upperBoundOnMessagesFetchedFromTheRoom": "",
"upToDate": "최신 버전입니다 ✓",
"useEphemeralTaskWorkerAgents": "",
"warnOnTheBoardWhenTodoWorkExceeds": "",
"whenEnabledDefaultFusionSpawnsShortLived": "",
"whenEnabledFusionChecksOpenAndClosedIssues": "",
@@ -6002,7 +6001,6 @@
"taskCreateWorkflowHelp": "",
"refinementTaskWorkflow": "",
"refinementTaskWorkflowHelp": "",
"ephemeralAgentsCompatibilityHint": "",
"maxRecommendationsPerTask": "",
"maxRecommendationsPerTaskHelp": ""
},

View File

@@ -5930,7 +5930,6 @@
"updating": "",
"upperBoundOnMessagesFetchedFromTheRoom": "",
"upToDate": "您已是最新版本 ✓",
"useEphemeralTaskWorkerAgents": "",
"warnOnTheBoardWhenTodoWorkExceeds": "",
"whenEnabledDefaultFusionSpawnsShortLived": "",
"whenEnabledFusionChecksOpenAndClosedIssues": "",
@@ -6002,7 +6001,6 @@
"taskCreateWorkflowHelp": "",
"refinementTaskWorkflow": "",
"refinementTaskWorkflowHelp": "",
"ephemeralAgentsCompatibilityHint": "",
"maxRecommendationsPerTask": "",
"maxRecommendationsPerTaskHelp": ""
},

View File

@@ -5930,7 +5930,6 @@
"updating": "",
"upperBoundOnMessagesFetchedFromTheRoom": "",
"upToDate": "您已是最新版本 ✓",
"useEphemeralTaskWorkerAgents": "",
"warnOnTheBoardWhenTodoWorkExceeds": "",
"whenEnabledDefaultFusionSpawnsShortLived": "",
"whenEnabledFusionChecksOpenAndClosedIssues": "",
@@ -6002,7 +6001,6 @@
"taskCreateWorkflowHelp": "",
"refinementTaskWorkflow": "",
"refinementTaskWorkflowHelp": "",
"ephemeralAgentsCompatibilityHint": "",
"maxRecommendationsPerTask": "",
"maxRecommendationsPerTaskHelp": ""
},

View File

@@ -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. ",