From 5acd8e987bef7dc10e1a07678a0d4094d53933f1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 17:28:14 -0700 Subject: [PATCH] =?UTF-8?q?fix(fnxc):=20three=20future-dated=20scheduler?= =?UTF-8?q?=20stamps=20=E2=80=94=20main=20red=20through=20three=20closed?= =?UTF-8?q?=20fixes=20(#3280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **`check-fnxc-future-dates` exits 1 on `origin/main`.** ``` packages/engine/src/scheduler.ts: 3 future-dated stamps, baseline allows 2 FNXC:ConcurrencyAdmission 2026-08-06-09:00 (six days out) FNXC:WorkflowLifecycleColumns 2026-08-01-05:00 FNXC:WorkflowScheduling 2026-08-01-01:05 ``` All three repointed to `2026-07-31`, times preserved. Gate now exits 0. ## This red has outlived three owners #3270, #3272 and #3274 were each opened against it and each **closed without merging**. Main has been red on this gate for hours while three fixes came and went. Claimed with `check-file-claimed.mjs` before starting — only #3262 touches `scheduler.ts`, and it is a terminal-role refactor rather than a stamp fix, so this was genuinely unowned. ## Why this keeps recurring Seven incidents in roughly two hours. The mechanism, in one line: **the date check runs only in CI** (`pr-checks.yml:66`, no pre-commit or pre-push hook), so every PR is validated against main's baseline *at its own CI time* and cannot see a concurrent or later change. Two PRs stamping the same file both pass, then compose into a red main. One case (#3273) was a stale branch **reverting** an already-merged fix. Patching instances has not converged — this PR is the eighth attempt at the same class. Two structural options, neither of which I am landing unilaterally since the second changes the gate's contract: - run the date check at **author time** (pre-push); it needs no baseline for "is this date in the future", so it cannot be raced - make the date rule **baseline-free** — a future-dated stamp is always wrong, unlike a lifecycle literal that may be a deliberate fallback `2026-08-06` being six days out also suggests these are not off-by-one timezone slips but stamps written from an intended future date. ## Verification - `check-fnxc-future-dates` — **exit 0** (was exit 1 on main) - `scheduler` suites — **148 pass** - `tsc --noEmit` (engine) — 0 errors - comment-only diff, no behaviour change Co-authored-by: Claude Opus 5 (1M context) --- packages/engine/src/scheduler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/scheduler.ts b/packages/engine/src/scheduler.ts index b0ececb1ee..d7de5c9c49 100644 --- a/packages/engine/src/scheduler.ts +++ b/packages/engine/src/scheduler.ts @@ -960,7 +960,7 @@ export class Scheduler { logger: schedulerLog, }); /* - FNXC:ConcurrencyAdmission 2026-08-06-09:00: + FNXC:ConcurrencyAdmission 2026-07-31-09:00: FN-8453's union must outlive a single scheduler poll. A temporary provider was gone before planning/merge asked for capacity, allowing newer work to overtake ready execute work. The refreshed map is the durable lane view. @@ -1404,7 +1404,7 @@ export class Scheduler { const deletedParked = await resolveTaskParkedColumns(this.store, task.id); /* - FNXC:WorkflowLifecycleColumns 2026-08-01-05:00: + FNXC:WorkflowLifecycleColumns 2026-07-31-05:00: A HALF-CONVERTED PAIR, one line apart. The hold read above already resolved its lane while the wip read below stayed on the literal, so on a renamed board this dependent sweep saw the queued cards and none of the running ones — a dependency held by an in-flight task was @@ -2255,7 +2255,7 @@ export class Scheduler { && typeof task.worktree === "string" && task.worktree.length > 0) .map((task) => task.id); /* - FNXC:WorkflowScheduling 2026-08-01-01:05 (self-deadlock in the widened ledger, observed live): + FNXC:WorkflowScheduling 2026-07-31-01:05 (self-deadlock in the widened ledger, observed live): A planned Ready card RETAINS its planning worktree for execution reuse, so counting it as a holder must not block ITS OWN release — on release the slot TRANSFERS (the card executes in the same worktree), it does not add. Without this exclusion the first unpause released only