FN-8221: clear inactive planner overseer state
Clear retained planner overseer state when effective oversight is disabled. - Remove monitor, recovery, advisor, and dedup runtime for oversight-off tasks. - Suppress stale oversight-off Eye badges in task cards. - Cover cleanup and badge behavior with regression tests. - Document the runtime snapshot invariant and add a patch changeset. Files changed: .changeset/fn-8221-overseer-badge-oversight-off.md | 7 ++ docs/architecture.md | 4 + packages/dashboard/app/components/TaskCard.tsx | 9 +- .../app/components/__tests__/TaskCard.test.tsx | 20 ++++ .../__tests__/planner-overseer-off-cleanup.test.ts | 119 +++++++++++++++++++++ packages/engine/src/project-engine.ts | 12 +++ 6 files changed, 170 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8221 Fusion-Task-Lineage: 01c9d838-fbe4-4d34-8eb5-d735cf35e581 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8221-overseer-badge-oversight-off.md
Normal file
7
.changeset/fn-8221-overseer-badge-oversight-off.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: The overseer eye badge no longer appears on in-progress/in-review tasks when oversight is off.
|
||||
category: fix
|
||||
dev: pollPlannerOverseer now clears retained PlannerOverseerMonitor observations (plus recovery/advisor runtime) when a task's effective plannerOversightLevel resolves to "off", so getPlannerOverseerRuntimeSnapshot returns null and TaskCard omits the Eye badge; TaskCard also guards on oversightLevel !== "off".
|
||||
@@ -1724,6 +1724,10 @@ block right beside it: any engine error is swallowed and the un-enriched list is
|
||||
no active observation omit the field entirely (byte-identical payload). `Task.plannerOverseerState?` is a
|
||||
transient field — engine-populated at serialization time, never persisted to the store or task.json.
|
||||
|
||||
When an in-flight task's effective planner oversight resolves to `"off"`,
|
||||
`ProjectEngine.pollPlannerOverseer` clears retained monitor observations and associated recovery/advisor
|
||||
runtime in that same poll. The snapshot therefore returns `null` rather than retaining a stale active state.
|
||||
|
||||
FN-7516's `TaskCard` renders the badge/affordance; this task only provides the field, the engine
|
||||
accessor, and (since FN-7516 had not yet landed consumption) a minimal guarded read plus a
|
||||
memo-comparator entry so the card repaints on state change.
|
||||
|
||||
@@ -3135,8 +3135,15 @@ function TaskCardComponent({
|
||||
`data-planner-overseer-state` attribute in TaskCard.css — do not fork the label
|
||||
logic here; `plannerOverseerStateLabel`/`plannerOverseerBadgeTooltip` remain the
|
||||
single source of truth.
|
||||
|
||||
FNXC:PlannerOversight 2026-07-17-00:00:
|
||||
FN-8221 defensively hides a stale non-idle snapshot when its oversight level is off.
|
||||
The engine clears this runtime at the source, but a client payload must never leak
|
||||
the Eye badge for an oversight-off in-progress or in-review task.
|
||||
*/}
|
||||
{task.plannerOverseerState && task.plannerOverseerState.state !== "idle" && (
|
||||
{task.plannerOverseerState
|
||||
&& task.plannerOverseerState.state !== "idle"
|
||||
&& task.plannerOverseerState.oversightLevel !== "off" && (
|
||||
<span
|
||||
className="card-status-badge card-planner-overseer-state"
|
||||
title={plannerOverseerBadgeTooltip(task.plannerOverseerState, t)}
|
||||
|
||||
@@ -320,6 +320,26 @@ describe("TaskCard", () => {
|
||||
expect(screen.getByTestId("planner-overseer-state-badge")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render an overseer badge for a stale non-idle oversight-off snapshot", () => {
|
||||
const staleOffTask = makeTask({
|
||||
column: "in-review",
|
||||
plannerOverseerState: {
|
||||
state: "watching",
|
||||
oversightLevel: "off",
|
||||
watchedStage: "reviewer",
|
||||
signal: "progressing",
|
||||
attemptCount: 0,
|
||||
attemptLimit: 3,
|
||||
pendingConfirmation: false,
|
||||
observedAt: 1700000000000,
|
||||
},
|
||||
});
|
||||
|
||||
render(<TaskCard task={staleOffTask} onOpenDetail={noop} addToast={noop} />);
|
||||
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// FN-7563: the badge used to print the raw kebab-case state (e.g.
|
||||
// "awaiting-confirmation") with a bare "Planner overseer: awaiting-confirmation"
|
||||
// tooltip. This reproduces the reported in-review symptom and asserts the badge
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* FNXC:PlannerOversight 2026-07-17-00:00:
|
||||
* FN-8221 exercises ProjectEngine's real poll seam with a narrow in-memory
|
||||
* workflow-settings store. The retained monitor observation must be cleared
|
||||
* whenever effective oversight changes to off, regardless of active column or
|
||||
* whether the value came from the task override or workflow setting.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { ProjectEngine } from "../project-engine.js";
|
||||
import { PlannerOverseerMonitor } from "../planner-overseer.js";
|
||||
|
||||
type PollStore = {
|
||||
tasks: Task[];
|
||||
workflowValues: Record<string, unknown>;
|
||||
listTasks(input: { column: string }): Promise<Task[]>;
|
||||
getSettings(): Promise<Record<string, unknown>>;
|
||||
getTaskWorkflowSelection(taskId: string): { workflowId: string; stepIds: string[] } | undefined;
|
||||
getWorkflowDefinition(id: string): Promise<undefined>;
|
||||
getWorkflowSettingsProjectId(): string;
|
||||
getWorkflowSettingValues(workflowId: string, projectId: string): Record<string, unknown>;
|
||||
};
|
||||
|
||||
function makeStore(tasks: Task[]): PollStore {
|
||||
return {
|
||||
tasks,
|
||||
workflowValues: {},
|
||||
async listTasks({ column }) {
|
||||
return this.tasks.filter((task) => task.column === column);
|
||||
},
|
||||
async getSettings() {
|
||||
return {};
|
||||
},
|
||||
getTaskWorkflowSelection() {
|
||||
return { workflowId: "builtin:coding", stepIds: [] };
|
||||
},
|
||||
async getWorkflowDefinition() {
|
||||
return undefined;
|
||||
},
|
||||
getWorkflowSettingsProjectId() {
|
||||
return "fn-8221";
|
||||
},
|
||||
getWorkflowSettingValues() {
|
||||
return this.workflowValues;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeTask(id: string, column: "in-progress" | "in-review"): Task {
|
||||
return {
|
||||
id,
|
||||
title: id,
|
||||
description: "Planner overseer cleanup fixture",
|
||||
column,
|
||||
status: column,
|
||||
priority: "normal",
|
||||
createdAt: "2026-07-17T00:00:00.000Z",
|
||||
updatedAt: "2026-07-17T00:00:00.000Z",
|
||||
} as Task;
|
||||
}
|
||||
|
||||
type PollEngine = {
|
||||
pollPlannerOverseer(store: PollStore): Promise<void>;
|
||||
getPlannerOverseerRuntimeSnapshot(taskId: string): ReturnType<ProjectEngine["getPlannerOverseerRuntimeSnapshot"]>;
|
||||
};
|
||||
|
||||
function makeEngine(): PollEngine {
|
||||
const engine = Object.create(ProjectEngine.prototype) as PollEngine;
|
||||
(engine as any).plannerOverseer = new PlannerOverseerMonitor();
|
||||
(engine as any).shuttingDown = false;
|
||||
(engine as any).plannerRecoveryController = undefined;
|
||||
(engine as any).sessionAdvisor = undefined;
|
||||
(engine as any).sessionAdvisorLogCursor = new Map();
|
||||
(engine as any).plannerObservationEmitDedup = new Map();
|
||||
(engine as any).plannerEscalationEmitDedup = new Set();
|
||||
return engine;
|
||||
}
|
||||
|
||||
describe("FN-8221 — oversight-off poll cleanup", () => {
|
||||
it.each([
|
||||
{ column: "in-progress" as const, source: "per-task override" as const },
|
||||
{ column: "in-review" as const, source: "per-task override" as const },
|
||||
{ column: "in-progress" as const, source: "workflow-effective setting" as const },
|
||||
{ column: "in-review" as const, source: "workflow-effective setting" as const },
|
||||
])("clears a retained snapshot when $source resolves to off in $column", async ({ column, source }) => {
|
||||
const task = makeTask(`${source}-${column}`, column);
|
||||
const store = makeStore([task]);
|
||||
const engine = makeEngine();
|
||||
|
||||
await engine.pollPlannerOverseer(store);
|
||||
expect(engine.getPlannerOverseerRuntimeSnapshot(task.id)).toMatchObject({
|
||||
oversightLevel: "autonomous",
|
||||
state: "watching",
|
||||
});
|
||||
|
||||
if (source === "per-task override") {
|
||||
task.plannerOversightLevel = "off";
|
||||
} else {
|
||||
store.workflowValues.plannerOversightLevel = "off";
|
||||
}
|
||||
|
||||
await engine.pollPlannerOverseer(store);
|
||||
expect(engine.getPlannerOverseerRuntimeSnapshot(task.id)).toBeNull();
|
||||
});
|
||||
|
||||
it("retains a non-idle snapshot while oversight remains active", async () => {
|
||||
const task = makeTask("active-oversight", "in-progress");
|
||||
const engine = makeEngine();
|
||||
|
||||
await engine.pollPlannerOverseer(makeStore([task]));
|
||||
await engine.pollPlannerOverseer(makeStore([task]));
|
||||
|
||||
expect(engine.getPlannerOverseerRuntimeSnapshot(task.id)).toMatchObject({
|
||||
oversightLevel: "autonomous",
|
||||
state: "watching",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2807,6 +2807,18 @@ export class ProjectEngine {
|
||||
workflowEffective.plannerOversightLevel as string | undefined,
|
||||
);
|
||||
if (level === "off") {
|
||||
/*
|
||||
* FNXC:PlannerOversight 2026-07-17-00:00:
|
||||
* FN-8221 requires tasks whose effective oversight resolves to off to discard retained
|
||||
* observations plus recovery/advisor runtime. This makes getPlannerOverseerRuntimeSnapshot
|
||||
* return null and prevents the TaskCard eye badge on oversight-off in-progress/in-review tasks.
|
||||
*/
|
||||
overseer.clear(task.id);
|
||||
this.plannerRecoveryController?.clear(task.id);
|
||||
this.sessionAdvisor?.clear(task.id);
|
||||
this.sessionAdvisorLogCursor.delete(task.id);
|
||||
this.plannerObservationEmitDedup.delete(task.id);
|
||||
this.clearPlannerEscalationDedup(task.id);
|
||||
continue;
|
||||
}
|
||||
// FN-7743: resolve the executor-stall threshold from the task's
|
||||
|
||||
Reference in New Issue
Block a user