feat(FN-3567): sync allowParallelExecution runtime toggle

Fixes FN-3567 by synchronizing the `allowParallelExecution` runtime toggle in the AgentDetailView component, adding one line to keep the setting in sync.

Fusion-Task-Id: FN-3567
This commit is contained in:
Fusion
2026-05-06 03:29:36 -07:00
committed by gsxdsm
parent 35d5590d4c
commit f69fb2fc2d
9 changed files with 43 additions and 25 deletions

View File

@@ -237,6 +237,7 @@ The `runtimeConfig` field on agents supports the following options:
| `autoClaimRelevantTasks` | `boolean` | `true` | During no-task heartbeats, opportunistically claim unowned relevant todo tasks that align with the agent's role/soul | | `autoClaimRelevantTasks` | `boolean` | `true` | During no-task heartbeats, opportunistically claim unowned relevant todo tasks that align with the agent's role/soul |
| `heartbeatTimeoutMs` | `number` | — | Time without heartbeat before agent is considered unresponsive (ms) | | `heartbeatTimeoutMs` | `number` | — | Time without heartbeat before agent is considered unresponsive (ms) |
| `maxConcurrentRuns` | `number` | `1` | Max concurrent heartbeat runs for this agent | | `maxConcurrentRuns` | `number` | `1` | Max concurrent heartbeat runs for this agent |
| `allowParallelExecution` | `boolean` | `true` (when unset) | Permanent agents only. When `false`, heartbeat and executor paths serialize symmetrically: a heartbeat will not start while the agent's bound task has an active executor session, and an executor session will not start while the agent has an active heartbeat run |
| `messageResponseMode` | `"immediate" \| "on-heartbeat"` | `"immediate"` | Whether agent wakes immediately on message (immediate) or processes during heartbeat (on-heartbeat). See [Heartbeat Run Mailbox Checking](#heartbeat-run-mailbox-checking) | | `messageResponseMode` | `"immediate" \| "on-heartbeat"` | `"immediate"` | Whether agent wakes immediately on message (immediate) or processes during heartbeat (on-heartbeat). See [Heartbeat Run Mailbox Checking](#heartbeat-run-mailbox-checking) |
| `selfImproveEnabled` | `boolean` | `true` | Enable periodic self-improvement reflection prompts during heartbeat runs | | `selfImproveEnabled` | `boolean` | `true` | Enable periodic self-improvement reflection prompts during heartbeat runs |
| `selfImproveIntervalMs` | `number` | `14400000` (4h) | Minimum delay between self-improvement cycles (minimum enforced: 3600000 ms) | | `selfImproveIntervalMs` | `number` | `14400000` (4h) | Minimum delay between self-improvement cycles (minimum enforced: 3600000 ms) |
@@ -248,6 +249,8 @@ The `runtimeConfig` field on agents supports the following options:
Heartbeat values are validated and minimum-clamped to 5 minutes (300,000 ms). Heartbeat values are validated and minimum-clamped to 5 minutes (300,000 ms).
Project setting `heartbeatMultiplier` (default `1`) scales resolved heartbeat intervals globally; per-agent `heartbeatIntervalMs` remains the base interval before multiplier scaling. This setting is configured from the **Agents** screen's **Controls** popup under "Heartbeat Speed". Project setting `heartbeatMultiplier` (default `1`) scales resolved heartbeat intervals globally; per-agent `heartbeatIntervalMs` remains the base interval before multiplier scaling. This setting is configured from the **Agents** screen's **Controls** popup under "Heartbeat Speed".
`allowParallelExecution` defaults to `true` when unset; setting it to `false` is serialized explicitly so operators can enforce non-parallel heartbeat/executor behavior for that permanent agent.
### No-task auto-claim behavior ### No-task auto-claim behavior
When an identity-bearing, non-ephemeral agent wakes with no assigned task and `runtimeConfig.autoClaimRelevantTasks !== false`, the heartbeat monitor scans open todo tasks and may claim one before constructing the prompt run. When an identity-bearing, non-ephemeral agent wakes with no assigned task and `runtimeConfig.autoClaimRelevantTasks !== false`, the heartbeat monitor scans open todo tasks and may claim one before constructing the prompt run.

View File

@@ -131,7 +131,7 @@ Todo View is an experimental dashboard surface for managing per-project todo lis
> Available when `experimentalFeatures.todoView` is enabled. > Available when `experimentalFeatures.todoView` is enabled.
Navigation: Navigation:
- Desktop: **Header → More views → Todos** - Desktop: **Header → More views → Todos** (single canonical desktop entry)
- Mobile: **More** sheet → **Todos** - Mobile: **More** sheet → **Todos**
For full behavior, API contracts, and storage details, use the canonical [Todo View guide](./todo-view.md). For full behavior, API contracts, and storage details, use the canonical [Todo View guide](./todo-view.md).

View File

@@ -751,6 +751,8 @@ Common heartbeat/runtime keys on `runtimeConfig` include:
| `heartbeatTimeoutMs` | `number` | Per-agent heartbeat timeout | | `heartbeatTimeoutMs` | `number` | Per-agent heartbeat timeout |
| `maxConcurrentRuns` | `number` | Per-agent concurrent heartbeat limit | | `maxConcurrentRuns` | `number` | Per-agent concurrent heartbeat limit |
| `messageResponseMode` | `"immediate" \| "on-heartbeat"` | Wake on message immediately or process during periodic heartbeat | | `messageResponseMode` | `"immediate" \| "on-heartbeat"` | Wake on message immediately or process during periodic heartbeat |
| `runMissedHeartbeatOnStartup` | `boolean` | Fire one startup catch-up heartbeat when the last tick was missed while the server was down (default `false`) |
| `allowParallelExecution` | `boolean` | Permanent agents only. Default `true` when unset. Set `false` to serialize heartbeat and executor sessions symmetrically (heartbeat won't start while executor is active, and executor won't start while heartbeat is active); `false` is explicitly persisted while unset/`true` keeps parallel behavior |
| `selfImproveEnabled` | `boolean` | Enables periodic self-improvement prompts | | `selfImproveEnabled` | `boolean` | Enables periodic self-improvement prompts |
| `selfImproveIntervalMs` | `number` | Delay between self-improvement cycles (default 4h, minimum 1h) | | `selfImproveIntervalMs` | `number` | Delay between self-improvement cycles (default 4h, minimum 1h) |
| `lastSelfImproveAt` | `string` | Last self-improvement checkpoint timestamp (managed by heartbeat monitor) | | `lastSelfImproveAt` | `string` | Last self-improvement checkpoint timestamp (managed by heartbeat monitor) |

View File

@@ -37,7 +37,7 @@ Behavior when disabled:
When enabled: When enabled:
- Desktop: header overflow menu (**More views**) → **Todos** - Desktop: header overflow menu (**More views**) → **Todos** (the only desktop Todos navigation entry)
- Mobile: **More** sheet in the mobile nav bar → **Todos** - Mobile: **More** sheet in the mobile nav bar → **Todos**
## List management ## List management

View File

@@ -3306,6 +3306,7 @@ function ConfigTab({
setHeartbeatEnabled(deriveHeartbeatEnabled(agent.runtimeConfig)); setHeartbeatEnabled(deriveHeartbeatEnabled(agent.runtimeConfig));
setAutoClaimRelevantTasksEnabled(deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig)); setAutoClaimRelevantTasksEnabled(deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig));
setRunMissedHeartbeatOnStartup(deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig)); setRunMissedHeartbeatOnStartup(deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig));
setAllowParallelExecution(deriveAllowParallelExecution(agent.runtimeConfig));
setBudgetValues(deriveBudgetValues(agent.runtimeConfig)); setBudgetValues(deriveBudgetValues(agent.runtimeConfig));
setModelValue(initialModelValue); setModelValue(initialModelValue);
setSelectedRuntimeId(initialRuntimeHint); setSelectedRuntimeId(initialRuntimeHint);

View File

@@ -1473,17 +1473,6 @@ export function Header({
</button> </button>
)} )}
{!isCompact && todosEnabled && onOpenTodos && (
<button
className={`btn-icon${todosOpen ? " btn-icon--active" : ""}`}
onClick={onOpenTodos}
title="Open todos"
data-testid="todos-toggle-btn"
>
<CheckSquare size={16} />
</button>
)}
{/* Git Manager button - desktop only (moved to overflow on mobile/tablet) */} {/* Git Manager button - desktop only (moved to overflow on mobile/tablet) */}
{!isCompact && onOpenGitManager && ( {!isCompact && onOpenGitManager && (
<button <button

View File

@@ -1687,10 +1687,11 @@ describe("App view switching", () => {
render(<App />); render(<App />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("todos-toggle-btn")).toBeInTheDocument(); expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeInTheDocument();
}); });
fireEvent.click(screen.getByTestId("todos-toggle-btn")); fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-todos"));
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("todo-view")).toBeInTheDocument(); expect(screen.getByTestId("todo-view")).toBeInTheDocument();

View File

@@ -381,21 +381,27 @@ describe("Header", () => {
}); });
}); });
describe("todos button", () => { describe("todos navigation", () => {
it("renders todos button on desktop when enabled", () => { it("shows Todos only in More views on desktop when enabled", () => {
renderHeader({ onOpenTodos: vi.fn(), todosEnabled: true }, "desktop"); renderHeader({ onChangeView: noop, onOpenTodos: vi.fn(), todosEnabled: true }, "desktop");
expect(screen.getByTitle("Open todos")).toBeDefined(); expect(screen.queryByTestId("todos-toggle-btn")).toBeNull();
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getAllByText("Todos")).toHaveLength(1);
expect(screen.getByTestId("view-overflow-todos")).toBeInTheDocument();
}); });
it("does not render todos button when disabled", () => { it("does not show Todos entry in More views when disabled", () => {
renderHeader({ onOpenTodos: vi.fn(), todosEnabled: false }, "desktop"); renderHeader({ onChangeView: noop, onOpenTodos: vi.fn(), todosEnabled: false }, "desktop");
expect(screen.queryByTitle("Open todos")).toBeNull(); fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-todos")).toBeNull();
}); });
it("calls onOpenTodos when clicked", () => { it("calls onOpenTodos from More views", () => {
const onOpenTodos = vi.fn(); const onOpenTodos = vi.fn();
renderHeader({ onOpenTodos, todosEnabled: true }, "desktop"); renderHeader({ onChangeView: noop, onOpenTodos, todosEnabled: true }, "desktop");
fireEvent.click(screen.getByTitle("Open todos")); fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-todos"));
expect(onOpenTodos).toHaveBeenCalled(); expect(onOpenTodos).toHaveBeenCalled();
}); });
}); });

View File

@@ -105,6 +105,22 @@ describe("MobileNavBar", () => {
expect(screen.queryByTestId("mobile-nav-tab-skills")).toBeNull(); expect(screen.queryByTestId("mobile-nav-tab-skills")).toBeNull();
}); });
it("keeps Todos in the mobile More sheet when todoView is enabled", () => {
const onOpenTodos = vi.fn();
render(
<MobileNavBar
{...createDefaultProps()}
onOpenTodos={onOpenTodos}
experimentalFeatures={{ todoView: true }}
/>,
);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
fireEvent.click(screen.getByTestId("mobile-more-item-todos"));
expect(onOpenTodos).toHaveBeenCalled();
});
it("renders dependency graph as a top-level tab and keeps additional plugin views in More", () => { it("renders dependency graph as a top-level tab and keeps additional plugin views in More", () => {
const props = createDefaultProps(); const props = createDefaultProps();
render( render(