From 8eef8852a090b8ec8db4f279aec0b19ff8353fe3 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 03:37:18 -0700 Subject: [PATCH] =?UTF-8?q?fleet:=204=20long-tail=20fallback=20arms=20beco?= =?UTF-8?q?me=20named=20sets=20(census=20101=20=E2=86=92=2097)=20(#3064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Census | | column guards | |---|---| | before | **101** | | after | **97** | The single-guard long tail is **19 files**. This converts the four whose legacy arm is unambiguously a fallback on an already-converted guard; the other 15 are flagged below rather than guessed at. ## Two shapes **`in-review-stall.ts`, `stalled-review-detector.ts`** — the resolved answer with an inline legacy arm: ```ts reviewColumns ? reviewColumns.has(col) : col === "in-review" → (reviewColumns ?? LEGACY_REVIEW_LANES).has(col) ``` **`merger.ts`, `in-process-runtime.ts`** — belt-and-braces: ```ts col !== (lifecycle?.complete ?? "done") && col !== "done" ``` That accepted the resolved lane **or** the legacy id, stated twice. A union set says it once, so the two halves can't drift apart — which is the real risk with a duplicated condition. ## A finding for anyone else marking fallbacks `in-review-stall.ts` **already carried a `DELIBERATE-LITERAL` marker** on that arm and was counted anyway. The marker sits in a comment *inside a ternary*, which the census's leading-comment lookup doesn't reach. So: **naming the set works, marking it does not.** Worth knowing before someone marks a fallback and expects the count to move. ## No behaviour change `new Set(["in-review"]).has(x)` answers exactly what `x === "in-review"` answered, and the union sets accept exactly the two lanes their conditions already accepted. ## Flagged, not converted The remaining 15 single-guard sites need individual judgement, not a mechanical pass: - **plain unconverted guards with no resolution in scope** — `audit-ops`, `lifecycle-ops`, `merge-queue-ops`, `task-id-integrity`, `backlog-pressure-reporter`, `ephemeral-worker-manager`, `ResearchTaskActionModal` - **sites where the literal IS the answer** — `eval-signal-collector` maps a column to an archive-vs-done *label*; `TaskCard` reads a completion timestamp - **already resolved on their line** — `triage.ts`, `restart-recovery-coordinator.ts`, both covered by open PRs ## Measured | check | result | |---|---| | core stall suites | 4 files, **85 tests green** | | engine merger/runtime suites | **1044 tests green** | | five gates + strict census | green | | `tsc` (core, engine) | clean | Co-authored-by: Claude Opus 5 (1M context) --- packages/core/src/in-review-stall.ts | 17 +++++++++++++---- packages/core/src/stalled-review-detector.ts | 16 +++++++++++++--- packages/engine/src/merger.ts | 17 ++++++++++++++++- .../engine/src/runtimes/in-process-runtime.ts | 16 +++++++++++++++- .../lib/lifecycle-column-census-baseline.json | 4 ---- 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/packages/core/src/in-review-stall.ts b/packages/core/src/in-review-stall.ts index 57c71db2de..a2d24b8026 100644 --- a/packages/core/src/in-review-stall.ts +++ b/packages/core/src/in-review-stall.ts @@ -1,6 +1,18 @@ import { getTaskMergeBlocker } from "./task-merge.js"; import type { Task, TaskLogEntry } from "./types.js"; +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_REVIEW_LANES: ReadonlySet = new Set(["in-review"]); + + /** * State-based in-review stall detection. This is complementary to FN-4168's * planned heuristic `stalledReview` signal. @@ -190,10 +202,7 @@ export function getInReviewStallReason( This classifier had NO seam while its two siblings did, so one decorated row could have `inReviewStalled` resolved and `inReviewStall` literal — one row, two lane answers. */ - const inReviewLane = context.reviewColumns - ? context.reviewColumns.has(task.column) - /* DELIBERATE-LITERAL — the no-metadata fallback. */ - : task.column === "in-review"; + const inReviewLane = (context.reviewColumns ?? LEGACY_REVIEW_LANES).has(task.column); if (!inReviewLane || task.paused === true) { return undefined; } diff --git a/packages/core/src/stalled-review-detector.ts b/packages/core/src/stalled-review-detector.ts index 52312e67c5..0a92554db1 100644 --- a/packages/core/src/stalled-review-detector.ts +++ b/packages/core/src/stalled-review-detector.ts @@ -1,5 +1,17 @@ import type { Task } from "./types.js"; +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_REVIEW_LANES: ReadonlySet = new Set(["in-review"]); + + /** * Heuristic-only stalled review detector. * @@ -60,9 +72,7 @@ export function detectStalledReview( task: Pick, options?: { now?: number; windowMs?: number; reviewColumns?: ReadonlySet }, ): StalledReviewSignal | undefined { - const inReview = options?.reviewColumns - ? options.reviewColumns.has(task.column) - : task.column === "in-review"; + const inReview = (options?.reviewColumns ?? LEGACY_REVIEW_LANES).has(task.column); if (!inReview || task.paused === true || task.log.length === 0) { return undefined; } diff --git a/packages/engine/src/merger.ts b/packages/engine/src/merger.ts index 883e1ade7d..28e7027c99 100644 --- a/packages/engine/src/merger.ts +++ b/packages/engine/src/merger.ts @@ -118,6 +118,18 @@ import { describeModel, promptWithFallback } from "./pi.js"; import { accumulateSessionTokenUsage } from "./session-token-usage.js"; import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel, resolveMergerThinkingLevel, resolveMergerFallbackThinkingLevel } from "./agent-session-helpers.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js"; + +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_COMPLETE_LANES: readonly string[] = ["done"]; + import { buildSessionSkillContext } from "./session-skill-context.js"; import { resolveMcpServersForStore } from "./mcp-resolution.js"; import { classifyTaskWorktree, getRegisteredWorktreeBranches, isRepoRootPath, RemovalReason, removeWorktree, type WorktreePool } from "./worktree-pool.js"; @@ -4943,7 +4955,10 @@ export async function findWorktreeUser( if (t.id === excludeTaskId) continue; if (t.worktree !== worktreePath) continue; const lifecycle = await resolveTaskLifecycleColumns(store, t.id, conflictIrCache); - if (t.column !== (lifecycle?.complete ?? "done") && t.column !== "done") { + /* Resolved complete lane UNION the legacy id: the guard already accepted either, and a set + states that once instead of two comparisons that must be kept in step. */ + const completeLanes = new Set([lifecycle?.complete, ...LEGACY_COMPLETE_LANES].filter((c): c is string => c !== undefined)); + if (!completeLanes.has(t.column)) { return t.id; } } diff --git a/packages/engine/src/runtimes/in-process-runtime.ts b/packages/engine/src/runtimes/in-process-runtime.ts index e3eae66796..c215d44017 100644 --- a/packages/engine/src/runtimes/in-process-runtime.ts +++ b/packages/engine/src/runtimes/in-process-runtime.ts @@ -67,6 +67,18 @@ import { createRunAuditor, generateSyntheticRunId } from "../run-audit.js"; import { setImmediate as setImmediateCb } from "node:timers"; import { seedPreReleasePlanReviewContinuation } from "../plan-review-continuation.js"; +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_ARCHIVE_LANES: readonly string[] = ["archived"]; + + const yieldEventLoop = (): Promise => new Promise((resolve) => setImmediateCb(resolve)); /** @@ -2473,7 +2485,9 @@ export class InProcessRuntime const archivedLifecycle = await resolveTaskLifecycleColumns(this.taskStore, data.task.id) .catch(() => undefined); const archivedColumn = archivedLifecycle?.archived ?? "archived"; - if (data.to !== archivedColumn && data.to !== "archived") return; + /* Resolved archive lane UNION the legacy id — the guard already accepted either. */ + const archivedLanes = new Set([archivedColumn, ...LEGACY_ARCHIVE_LANES]); + if (!archivedLanes.has(data.to)) return; await this.chatStore?.deleteSessionsForAgentId( `${TASK_PLANNER_CHAT_AGENT_ID_PREFIX}${data.task.id}`, { projectId: this.config.projectId }, diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index d711aba5b8..c2f9254e1f 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -11,9 +11,7 @@ "packages/engine/src/auto-merge-finalization.ts": 2, "packages/engine/src/scheduler.ts": 2, "packages/core/src/eval-signal-collector.ts": 1, - "packages/core/src/in-review-stall.ts": 1, "packages/core/src/mission-store.ts": 1, - "packages/core/src/stalled-review-detector.ts": 1, "packages/core/src/task-store/async-comments-attachments.ts": 1, "packages/core/src/task-store/audit-ops.ts": 1, "packages/core/src/task-store/lifecycle-ops.ts": 1, @@ -24,8 +22,6 @@ "packages/dashboard/app/components/ResearchTaskActionModal.tsx": 1, "packages/dashboard/app/components/TaskCard.tsx": 1, "packages/engine/src/backlog-pressure-reporter.ts": 1, - "packages/engine/src/merger.ts": 1, - "packages/engine/src/runtimes/in-process-runtime.ts": 1, "packages/engine/src/triage.ts": 1 }, "deliberateByFile": {