From 5d2c3be6a070e649f72f20ac26dc5ad2407cb7d7 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 17 Jul 2026 11:27:44 -0700 Subject: [PATCH] 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) --- .../fn-8221-overseer-badge-oversight-off.md | 7 ++ docs/architecture.md | 4 + .../dashboard/app/components/TaskCard.tsx | 9 +- .../components/__tests__/TaskCard.test.tsx | 20 +++ .../planner-overseer-off-cleanup.test.ts | 119 ++++++++++++++++++ packages/engine/src/project-engine.ts | 12 ++ 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 .changeset/fn-8221-overseer-badge-oversight-off.md create mode 100644 packages/engine/src/__tests__/planner-overseer-off-cleanup.test.ts diff --git a/.changeset/fn-8221-overseer-badge-oversight-off.md b/.changeset/fn-8221-overseer-badge-oversight-off.md new file mode 100644 index 0000000000..7b840449af --- /dev/null +++ b/.changeset/fn-8221-overseer-badge-oversight-off.md @@ -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". diff --git a/docs/architecture.md b/docs/architecture.md index 1fb516d054..c45c7e9307 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 51dc1579ef..58307e36ca 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -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" && ( { 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(); + + 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 diff --git a/packages/engine/src/__tests__/planner-overseer-off-cleanup.test.ts b/packages/engine/src/__tests__/planner-overseer-off-cleanup.test.ts new file mode 100644 index 0000000000..61c78889d6 --- /dev/null +++ b/packages/engine/src/__tests__/planner-overseer-off-cleanup.test.ts @@ -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; + listTasks(input: { column: string }): Promise; + getSettings(): Promise>; + getTaskWorkflowSelection(taskId: string): { workflowId: string; stepIds: string[] } | undefined; + getWorkflowDefinition(id: string): Promise; + getWorkflowSettingsProjectId(): string; + getWorkflowSettingValues(workflowId: string, projectId: string): Record; +}; + +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; + getPlannerOverseerRuntimeSnapshot(taskId: string): ReturnType; +}; + +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", + }); + }); +}); diff --git a/packages/engine/src/project-engine.ts b/packages/engine/src/project-engine.ts index c0c6f3a208..910c1699f9 100644 --- a/packages/engine/src/project-engine.ts +++ b/packages/engine/src/project-engine.ts @@ -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