Skip to content

PlanForceBot consults the state-less ForcePlanBlockers overload: it cannot see apc_owns_it / forcing_failed_on_this_plan / already_forced before an automatic force #3654

Closed

Description

@erikdarlingdata

Summary

#3652 taught the force-plan verdict to read the store's forcing and automatic-plan-correction state at read time: FactRemediation.ForcePlanBlockers(ForcePlanTarget, ForcePlanTargetState?) adds apc_owns_it, already_forced, forcing_failed_on_this_plan, apc_withdrew_it and apc_resolved_differently, and both SKUs' MCP surfaces pass a per-target ForcePlanTargetState read by ForcePlanTargetStateReader (Lite, DuckDB) / DarlingForcePlanTargetStateReader (Darling, Postgres).

PlanForceBot (Darling/PerformanceMonitor.Darling.Service/PlanForceBot.cs ~157) still calls the ORIGINAL one-argument overload — the two #2138 blockers only — and hands that list to ForcePlanBotPolicy.Evaluate. So the automatic bot's gate has exactly the gap #3652 closed on the advisory surface: it would judge a plan APC is mid-verification on, a plan whose force_failure_count is already climbing, or a plan already forced by hand, as unblocked. The bot is dry-run-gated today (write-gate 2, V107), which is why this is a follow-up rather than part of #3652; it must not go live without this.

Fix shape

  • PlanForceBot reads the state for its candidate targets through DarlingForcePlanTargetStateReader.ReadAsync (it has the NpgsqlDataSource) and calls the two-argument overload; the journal row records the blocker names AND the evidence strings (ForcePlanBlocker.Evidence), so a Blocked decision is auditable against the snapshot it was made on.
  • When the state read fails, the bot treats it as a blocker of its own (state_unavailable) — the advisory surface can say "unknown" and let a human decide; an automatic actor cannot.
  • ForcePlanBotPolicy stays pure; it already takes the blocker list as data.
  • Pin: PlanForceBotTests gains the five Force-plan remediation reports 'eligible, no blockers' for plans automatic plan correction already owns, has already failed to force, or has withdrawn — five for five on one incident #3652 shapes and the unavailable-state arm; ForcePlanTargetStateTests.ThePlanForceBotsGate_IsTheOriginalOverload_AndStillAgreesWithTheTargetHalf currently pins the GAP (so it is a test, not a surprise) and flips when this lands.

Related: #3652 (the advisory-surface fix), #2138 (the bot), #3579 (Forced Plan Failing is the alert side of force_failure_count).

Activity

  1. erikdarlingdata commented on Sep 18, 2026

    @erikdarlingdata
    OwnerAuthor

    Closed by the merge of #3663 (its body names this issue with a closing keyword; keywords do not fire against dev, so the watcher closes it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions