Files
fusion/packages/engine/src/executor/requeue-loop.ts
gsxdsm 599a509d22 refactor: package code organization (god-file peels, wave 1) (#2139)
## Summary

First wave of package-internal code organization: split oversized
modules into domain-named files/folders while preserving public import
paths via re-exports, and refresh the line-count ratchet scoreboard.

- **Plan:**
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`
(multi-wave program; this PR lands U1–U4 + first U3/U6 slices)
- **Core types:** peel `types.ts` into
`types/{board,merge-queue,execution-and-ui,merge-policy,workflow-steps}.ts`
with browser-safe Vite alias preserved
- **Core TaskStore:** rename `remaining-ops-9` →
`task-commit-associations` (domain-named, not ordinal dump)
- **Engine executor:** peel pure helpers into
`executor/{browser-probe,requeue-loop,pseudo-pause,workflow-step-failures}.ts`
- **Engine heartbeat:** peel system prompts/procedures into
`agent-heartbeat-prompts.ts`
- **Ratchet:** one-time baseline truth-up + ratchet-down for touched
files

### Deferred to follow-up PRs (plan U5, U7–U9 + remaining waves)
- Self-healing folder split
- Further remaining-ops domain peels
- Dashboard `legacy.ts` / routes / UI monofiles
- CLI extension + TUI peels

## Test plan

- [x] `pnpm --filter @fusion/core exec tsc --noEmit`
- [x] `pnpm --filter @fusion/engine exec tsc --noEmit`
- [x] Focused vitest: `detect-pseudo-pause`,
`executor-browser-verification`, `clear-terminal-workflow-step-failures`
- [x] `node scripts/check-file-line-count.mjs` clean against updated
baseline
- [ ] CI merge gate (lint/typecheck/build/gate)
- [ ] Browser smoke: N/A for this PR (no dashboard UI route changes)

## Residual Review Findings

None. Review autofix applied dual-home wiring for
`clearTerminalWorkflowStepFailures` only.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added configurable heartbeat procedures for task and no-task scenarios
(including patrol-aware rendering).
* Improved agent-browser availability verification with clearer
availability/status reporting.
  * Added detection for pseudo-pauses and review-handoff requests.
* Expanded core configuration/contract options for
execution/UI/localization, merges, merge queues, and workflow steps.
* **Bug Fixes**
* Improved handling of transient execute-requeue and workflow-step
retry/cleanup behavior, including better Windows path support.
  * Preserved existing public interfaces during internal restructuring.
* **Documentation**
  * Added a multi-phase roadmap for future package reorganization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-15 13:34:30 -07:00

106 lines
5.0 KiB
TypeScript

/**
* FNXC:CodeOrganization 2026-07-15-00:00:
* Execute-node self-requeue loop signature helpers peeled from executor.ts.
*/
import type { TaskDetail, Task } from "@fusion/core";
import { TaskDeletedError } from "@fusion/core";
import { isAbsolute, relative, resolve as resolvePath } from "node:path";
/** Maximum no-progress execute-node self-requeues before terminalizing the loop. */
export const MAX_EXECUTE_REQUEUE_LOOP_CYCLES = 6;
/** Low-water mark for surfacing a visible warning before loop terminalization. */
export const EXECUTE_REQUEUE_LOOP_VISIBLE_THRESHOLD = 3;
function countExecuteRequeueTerminalSteps(live: TaskDetail): number {
return live.steps?.filter((step) => step.status === "done" || step.status === "skipped").length ?? 0;
}
function parseExecuteRequeueLoopProgressSignature(signature: string | null | undefined): { terminalStepCount: number; totalSteps: number } | null {
if (!signature) return null;
try {
const parsed = JSON.parse(signature) as { terminalStepCount?: unknown; totalSteps?: unknown };
if (typeof parsed.terminalStepCount !== "number" || typeof parsed.totalSteps !== "number") return null;
return {
terminalStepCount: parsed.terminalStepCount,
totalSteps: parsed.totalSteps,
};
} catch {
return null;
}
}
export function buildExecuteRequeueLoopSignature(live: TaskDetail): string {
/*
FNXC:WorkflowLifecycle 2026-07-13-07:42:
FN-7941: human reports #2043/#2045/#2046/#2047 showed that FN-7863's raw currentStep/status signature could drift on every execute self-requeue while no step reached a terminal state, resetting the loop counter to 1 forever. Anchor the bounded streak to monotonic terminal-step progress instead: pending/in-progress/currentStep oscillation still counts toward exhaustion, while real done/skipped progress resets the streak and FN-7926 still diverts completed-blocked work before this guard can fail it.
*/
return JSON.stringify({
terminalStepCount: countExecuteRequeueTerminalSteps(live),
totalSteps: live.steps?.length ?? 0,
});
}
export function buildExecuteRequeueLoopHighWaterSignature(live: TaskDetail, previousSignature: string | null | undefined): { signature: string; madeForwardProgress: boolean } {
// FNXC:WorkflowLifecycle 2026-07-13-08:20: derive current terminal-step
// progress by parsing buildExecuteRequeueLoopSignature's own output rather
// than duplicating countExecuteRequeueTerminalSteps/totalSteps inline, so
// the two functions cannot silently drift out of sync.
const current = parseExecuteRequeueLoopProgressSignature(buildExecuteRequeueLoopSignature(live));
const currentTerminalStepCount = current?.terminalStepCount ?? countExecuteRequeueTerminalSteps(live);
const totalSteps = current?.totalSteps ?? (live.steps?.length ?? 0);
const previous = parseExecuteRequeueLoopProgressSignature(previousSignature);
const previousTerminalStepCount = previous?.terminalStepCount ?? currentTerminalStepCount;
const madeForwardProgress = previous != null && currentTerminalStepCount > previousTerminalStepCount;
return {
madeForwardProgress,
signature: JSON.stringify({
terminalStepCount: Math.max(previousTerminalStepCount, currentTerminalStepCount),
totalSteps,
}),
};
}
const INVALID_ASSISTANT_CONTINUATION_PATTERN = /cannot continue from message role:\s*assistant/i;
export function isInvalidAssistantContinuationErrorMessage(errorMessage: string): boolean {
return INVALID_ASSISTANT_CONTINUATION_PATTERN.test(errorMessage);
}
export const TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN = /ENOENT:\s+no such file or directory,\s+open\s+'([^']+[\\/]\.fusion[\\/]tasks[\\/]([^\\/]+)[\\/]task\.json)'/;
function normalizeErrorPath(path: string): string {
return resolvePath(path.replace(/\\/g, "/"));
}
function isContainedByWorktree(filePath: string, worktree: string): boolean {
/*
FNXC:ExecutorRecovery 2026-07-15-13:20:
Transient task.json recovery must recognize Node ENOENT paths on both Windows and POSIX, but only when the missing file belongs to the task's exact worktree. Normalize separator-only error-message differences before path resolution, then use relative-path containment so sibling prefixes such as `fn-1-copy` cannot be mistaken for `fn-1`.
*/
const relativePath = relative(normalizeErrorPath(worktree), normalizeErrorPath(filePath));
return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
}
export function isTransientMissingTaskJsonError(error: unknown, task: Pick<Task, "id" | "worktree">): boolean {
if (error instanceof TaskDeletedError) {
return false;
}
const message = typeof error === "string"
? error
: error instanceof Error
? error.message
: "";
const match = TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN.exec(message);
if (!match) {
return false;
}
const [, filePath, taskIdFromPath] = match;
if (taskIdFromPath !== task.id) {
return false;
}
if (typeof task.worktree !== "string" || task.worktree.length === 0) {
return false;
}
return isContainedByWorktree(filePath, task.worktree);
}