fix: main is RED on two gates — record the new fallback, repoint six future-dated stamps (#3261)
`9094d1640e` (globalPause gates every graph node entry) reddened **two** lifecycle gates on main. Both are fixed here, in separate commits. ## 1. The census ratchet went 0 → 2 `isTerminalColumnTask` in `scheduler.ts`: ```ts const flags = columnFlagsForTask(task); if (flags) return flags.complete === true || flags.archived === true; return task.column === "done" || task.column === "archived"; // ← counted ``` **The code is correct.** It resolves traits first and falls back only when the workflow is unreadable. The census counts fallback literals on purpose — *"a fallback literal is still a literal and should go when the trait path becomes unconditional"* — and reports them beside the backlog as already-converted. Its own remedy for a legitimate one is a `DELIBERATE-LITERAL` marker at the site. Recorded rather than converted because **there is nothing to convert to**: a task whose workflow cannot be read has no resolved lane, and treating it as non-terminal would count a finished card's retained worktree against live capacity — the opposite of what the surrounding fix does. Marker sits in the declaration's **leading** comments; an inline one attaches to the wrong node and is silently ignored, which cost a miscount once before. Baseline re-recorded in the same commit, since the census tracks deliberate counts and reports a marker addition as `RECLASSIFIED`. ## 2. The stamp gate was red as well Six files stamped `2026-08-01-00:2x` while UTC was `2026-07-31`: ``` workflow-column-boundary.ts 2 workflow-graph-task-runner.ts 1 workflow-column-boundary-hooks.ts 1 in-process-runtime.ts 5 (allows 4) workflow-column-boundary-capacity.test 1 ``` This checkout is UTC-7, so "just after midnight local" is tomorrow in UTC — the case AGENTS.md documents, which passes `pnpm lint` locally *because* the local clock agrees with what was written. Second occurrence today; I fixed the same shape on #3208 for another worker. Repointed to `2026-07-31-22:2x`, preserving relative order. **Zero non-comment lines changed** — 8 lines across 6 files, verified by diffing out FNXC lines. ## Measured | check | before | after | |---|---|---| | `census --strict` | **1** | **0** | | backlog | **2** | **0** (DELIBERATE-LITERAL 148 → 150) | | `check-fnxc-future-dates` | **1** | **0** | | `pnpm test:gate` | 0 | 0 | | `census-reclassification-message` | 2 failed | **1 failed** | That last row is deliberate: the remaining failure is the expired-premise case #3260 fixes, and I have not touched it. The capacity test from `9094d1640e` still passes 9/9. ## Why this landed at all Both gates run in `pr-checks.yml`, so a PR carrying either would have gone red. Worth someone checking how it merged — a stale merge base would explain it, and if so the same hole is open for the next merge.
This commit is contained in:
@@ -57,7 +57,7 @@ function invariantError() {
|
||||
|
||||
describe("workflow column boundary — global pause suspends at every node entry", () => {
|
||||
/*
|
||||
FNXC:EnginePause 2026-08-01-00:30:
|
||||
FNXC:EnginePause 2026-07-31-22:30:
|
||||
Operator regression: Stop AI Engine (globalPause) did not stop the graph — a live run started a
|
||||
fresh Plan Review model session two minutes after pause, because no node boundary ever re-read
|
||||
settings. These fail if the `isPaused` probe is removed from onNodeEntry.
|
||||
|
||||
@@ -2309,7 +2309,7 @@ export class InProcessRuntime
|
||||
private async drainWorkflowContinuations(): Promise<void> {
|
||||
if (this.workflowContinuationDrainActive || this.status !== "active") return;
|
||||
/*
|
||||
FNXC:EnginePause 2026-08-01-00:20:
|
||||
FNXC:EnginePause 2026-07-31-22:20:
|
||||
A pause-suspended run persists a runnable continuation (same mechanism as capacity). Without
|
||||
this gate the drain would re-dispatch it on the next tick and the graph would bounce
|
||||
suspend→dispatch→suspend forever while paused — and worse, dispatch genuinely new work under
|
||||
|
||||
@@ -1277,7 +1277,7 @@ export class Scheduler {
|
||||
hottest write path (26 emit sites against 7, measured), not a signature change here. */
|
||||
if (task.column === "in-progress") this.failedTaskIds.add(task.id);
|
||||
/*
|
||||
FNXC:MissionReconciliation 2026-08-01-00:00:
|
||||
FNXC:MissionReconciliation 2026-07-31-22:00:
|
||||
In-place failure parks do not emit task:moved, but they release the
|
||||
task's durable symbol lock. Reconcile any mission-linked failure update
|
||||
so the roadmap records withheld provenance without fabricating completion.
|
||||
@@ -2230,6 +2230,19 @@ export class Scheduler {
|
||||
membership — wip cards without a worktree yet still reserve (they are about to acquire), and
|
||||
terminal lanes are excluded because their retained worktrees are cleanup-owned, not capacity.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-20:55 (u12 — the ratchet caught this, correctly):
|
||||
DELIBERATE-LITERAL — the unresolvable-workflow default. The trait path above is the real answer;
|
||||
this arm is reached only when `columnFlagsForTask` returns undefined, i.e. the task's workflow
|
||||
could not be read at all, and it then gives the same answer the pre-trait code gave.
|
||||
|
||||
Recorded rather than converted because there is nothing to convert TO: a task with no readable
|
||||
workflow has no resolved lane, and treating it as non-terminal would count a finished card's
|
||||
retained worktree against live capacity — the opposite of what the surrounding fix does.
|
||||
|
||||
Marker sits in the DECLARATION's leading comments, not inline: markers are read from a node's
|
||||
leading comments, so a mid-expression one attaches to the wrong node and is silently ignored.
|
||||
*/
|
||||
const isTerminalColumnTask = (task: Task): boolean => {
|
||||
const flags = columnFlagsForTask(task);
|
||||
if (flags) return flags.complete === true || flags.archived === true;
|
||||
|
||||
@@ -61,7 +61,7 @@ export function createExecutorColumnBoundaryHooks(
|
||||
// KTD-3 drift-park loop fix (PR #2342): detectDrift clears the stale pin
|
||||
// row fields so an ordinary requeue re-resolves the CURRENT IR fresh.
|
||||
clearPin: pinPersistence.clearPin,
|
||||
/* FNXC:EnginePause 2026-08-01-00:20: settings re-read per node entry — event-independent. */
|
||||
/* FNXC:EnginePause 2026-07-31-22:20: settings re-read per node entry — event-independent. */
|
||||
isPaused: async () => {
|
||||
try {
|
||||
const settings = await store.getSettings();
|
||||
|
||||
@@ -103,7 +103,7 @@ export interface WorkflowColumnBoundaryDeps {
|
||||
/** Persist a durable continuation before control returns to the scheduler. */
|
||||
onSuspend?: (suspension: Extract<WorkflowColumnBoundaryEntryResult, { kind: "suspended" }>) => void | Promise<void>;
|
||||
/*
|
||||
FNXC:EnginePause 2026-08-01-00:20 (Stop AI Engine did not stop the graph):
|
||||
FNXC:EnginePause 2026-07-31-22:20 (Stop AI Engine did not stop the graph):
|
||||
Operator-observed regression: with `globalPause: true` the graph runner kept crossing node
|
||||
boundaries — a live run started a NEW Plan Review step (fresh model session) two minutes after
|
||||
Stop AI Engine, and the plan-review→replan loop kept cycling "attempt N/unbounded". The legacy
|
||||
@@ -290,7 +290,7 @@ export function createWorkflowColumnBoundary(
|
||||
const toColumn = node.column;
|
||||
|
||||
/*
|
||||
FNXC:EnginePause 2026-08-01-00:20:
|
||||
FNXC:EnginePause 2026-07-31-22:20:
|
||||
Pause gates EVERY node entry — columnless and same-column nodes included, because each node
|
||||
can start a real AI session regardless of whether the card moves. Suspend with the same
|
||||
durable-continuation mechanism capacity uses, so unpause resumes at exactly this node; the
|
||||
|
||||
@@ -177,7 +177,7 @@ export interface WorkflowColumnBoundaryHooks {
|
||||
clearPin?: () => void | Promise<void>;
|
||||
onWarn?: (message: string, detail: Record<string, unknown>) => void;
|
||||
onSuspend?: WorkflowColumnBoundaryDeps["onSuspend"];
|
||||
/** FNXC:EnginePause 2026-08-01-00:20: polled at every node entry (see boundary deps). */
|
||||
/** FNXC:EnginePause 2026-07-31-22:20: polled at every node entry (see boundary deps). */
|
||||
isPaused?: WorkflowColumnBoundaryDeps["isPaused"];
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"packages/engine/src/restart-recovery-coordinator.ts": 1,
|
||||
"packages/engine/src/runtime-resolution.ts": 1,
|
||||
"packages/engine/src/runtimes/in-process-runtime.ts": 4,
|
||||
"packages/engine/src/scheduler.ts": 3,
|
||||
"packages/engine/src/scheduler.ts": 2,
|
||||
"packages/engine/src/triage.ts": 6,
|
||||
"packages/engine/src/workflow-work-processor.ts": 1,
|
||||
"scripts/lib/lifecycle-column-census-ast.mjs": 3,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"deliberateByFile": {
|
||||
"packages/core/src/task-store/async-comments-attachments.ts\u0000archived": 6,
|
||||
"packages/dashboard/app/components/TaskContextMenu.tsx\u0000in-review": 3,
|
||||
"packages/engine/src/scheduler.ts\u0000archived": 3,
|
||||
"packages/engine/src/scheduler.ts\u0000done": 3,
|
||||
"packages/engine/src/scheduler.ts\u0000in-progress": 3,
|
||||
"packages/engine/src/scheduler.ts\u0000in-review": 3,
|
||||
"packages/engine/src/self-healing.ts\u0000in-review": 3,
|
||||
@@ -26,8 +28,6 @@
|
||||
"packages/dashboard/src/reliability-metrics.ts\u0000in-review": 2,
|
||||
"packages/engine/src/auto-merge-finalization.ts\u0000done": 2,
|
||||
"packages/engine/src/cli-agent/state-machine.ts\u0000done": 2,
|
||||
"packages/engine/src/scheduler.ts\u0000archived": 2,
|
||||
"packages/engine/src/scheduler.ts\u0000done": 2,
|
||||
"packages/engine/src/self-healing.ts\u0000done": 2,
|
||||
"packages/engine/src/triage.ts\u0000triage": 2,
|
||||
"packages/engine/src/usage-limit-detector.ts\u0000archived": 2,
|
||||
|
||||
Reference in New Issue
Block a user