Files
fusion/packages/engine/src/executor/task-executor-state.ts
gsxdsm 56a087dac9 fix(engine): restore shipped behaviors dropped by the wave-18 executor peel
Full-suite repair, engine harness cluster (~85 red suites). Two causes:
(1) graph dispatch now fails closed without options.agentStore
(FN-8764/FN-8821, intended) — the shared executor test harness now
provisions the workflow-routing agent-store fixture for bare
TaskExecutor constructions, with explicit opt-out for the two tests
asserting the fail-closed park; (2) the wave-18 'pure peel' (#3317)
rebuilt executor.ts from a stale base and silently deleted shipped
behaviors, restored here: FN-8864 agent-activity writers (task
started/handed-off, workflow gate pass/fail, gate principal attribution
via executor/workflow-gate-activity.ts), FN-8768 Plan Review group
recognition, convergence primer, and modified-file review scoping,
FN-6782's fire-time guard on transient resume-after-restart retries,
FN-8868 session usage telemetry boundaries, recommendation-route
withheld-tool guidance, and the per-instance worktree retry cap.
Stale expectations updated for intended changes (FN-8823 shared-member
hold, FN-9060 zero-acquire fail-closed, heartbeat tool inventory,
peeled-module seams, PG harness provisioning). Verified: 23 files /
711 tests green, engine typecheck clean, merge gate green.

Known follow-ups (not addressed here): step-session error routing may
still bypass FN-5866 non-continuable classification
(post-done-continuation-no-wedge red), scheduler mission-loop trigger
gap (mission-validation-trigger-gap red), and executeWorkflowStep lost
routed workflow-principal session identity threading (untested drop
from the same peel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 15:52:52 -07:00

147 lines
8.1 KiB
TypeScript

/**
* FNXC:CodeOrganization 2026-08-04-07:10:
* TaskExecutor instance state fields peeled to a base class (U4) so executor.ts keeps
* thin method facades only. Fields are protected (not private) so TaskExecutor methods
* and runtime tests that poke (executor as any).fieldName keep working.
*/
import type {
Agent,
AgentStore,
TaskStore,
RunMutationContext,
MergeResult,
ThinkingLevel,
WorkflowColumnAgent,
ApprovalRequestStore,
WorkspaceConfig,
} from "@fusion/core";
import type { ImplementationExit } from "./implementation-exit.js";
import type { ForeachActiveContext } from "../workflows/workflow-node-handlers.js";
import { ModelRegistry, type AgentSession } from "@earendil-works/pi-coding-agent";
import { CliTaskSession } from "../cli-agent/task-session.js";
import { TokenCapDetector } from "../errors/token-cap-detector.js";
import { StepSessionExecutor } from "../execution/step-session-executor.js";
import type { PausedAbortProvenance } from "./paused-abort-provenance.js";
import type { ActiveExecutorSessionState, TaskExecutorOptions } from "./task-executor-options.js";
import type { WorkflowAgentCapacity } from "../agents/workflow-agent-capacity.js";
import { invalidateWorkspaceConfigCache } from "./workspace-config-resolver.js";
export abstract class TaskExecutorState {
/**
* FNXC:CodeOrganization 2026-08-04-08:05:
* Constructor-injected store/rootDir/options live on the state base (U4) so pure
* worktree facades and bags can share them without TaskExecutor parameter properties.
*/
protected store!: TaskStore;
protected rootDir!: string;
protected options: TaskExecutorOptions = {};
protected activeWorktrees = new Map<string, Set<string>>();
/**
* FNXC:WorkflowAgentRouting 2026-08-07-03:46:
* Workflow stage reservations are intentionally independent from heartbeat slots.
* Constructed in wireTaskExecutorLifecycle so direct graph admission and triage share the same class.
*/
protected workflowAgentCapacity!: WorkflowAgentCapacity;
/**
* FNXC:WorkflowAgentRouting 2026-08-07-03:46:
* Process-local index carrying a durable work item's narrow authority to the model tool gate.
* Keyed by task for the live graph turn; removed in graph cleanup. isLive revalidates the exact record.
*/
protected activeWorkflowAuthorities = new Map<string, {
agentId: string;
taskId: string;
runId: string;
workItemId: string;
nodeInstanceId: string;
/** Direct graph runs have no work-item row; claimed continuations must recheck it per tool call. */
requiresDurableFence: boolean;
kind: "task-assignee" | "review-node-override";
}>();
/**
* FNXC:WorkflowAgentRouting 2026-08-07-03:46:
* Live fenced principal per task for execute/review session identity (exact graph-selected agent).
* `agent` is the admission-time row so session identity does not depend on a second getAgent round-trip.
*/
protected activeWorkflowPrincipals = new Map<string, { agentId: string; nodeInstanceId: string; agent?: Agent }>();
/**
* FNXC:AgentActivityStream 2026-08-09-13:59 (restored 2026-08-15-22:15 after wave-18 shell-ification dropped it):
* Node-scoped routed-principal retention (`taskId\0nodeId` -> agentId) that outlives release-principal,
* because the graph can emit a terminal gate result AFTER the per-attempt reservation is released while
* the activity outbox must still attribute that gate to the exact routed principal (FN-8864).
*/
protected workflowGateActivityPrincipals = new Map<string, string>();
protected executing = new Set<string>();
protected resumingUnpaused = new Set<string>();
protected approvalSuspended = new Set<string>();
protected approvalResumeAfterUnwind = new Set<string>();
protected recoveringCompleted = new Set<string>();
protected capturedReflectionTaskIds = new Set<string>();
protected workflowRerunPending = new Set<string>();
protected workflowLifecycleMovesInFlight = new Set<string>();
protected pendingTaskDisposals = new Map<string, Promise<void>>();
protected unregisterTaskMoveDisposer: (() => void) | undefined;
protected unregisterArchiveWorktreeDisposer: (() => void) | undefined;
protected unregisterArchiveWorkspaceWorktreeDisposer: (() => void) | undefined;
protected activeSessions = new Map<string, ActiveExecutorSessionState>();
protected activeStepExecutors = new Map<string, StepSessionExecutor>();
protected activeStepExecutorSeenSteeringIds = new Map<string, Set<string>>();
protected effectiveColumnAgentByTask = new Map<string, string>();
protected activeWorkflowStepSessions = new Map<string, AgentSession>();
protected activePlanningWorkflowSessions = new Set<string>();
protected activeWorkflowStepSessionSeenSteeringIds = new Map<string, Set<string>>();
protected activeConfiguredCommandControllers = new Map<string, Set<AbortController>>();
protected authoritativeAssignedAgentStore: AgentStore | null = null;
protected activeWorkflowGraphAbortControllers = new Map<string, AbortController>();
protected activeCliTaskSessions = new Map<string, CliTaskSession>();
protected readonlyWorkflowStepAuditDone = false;
protected activeSubagentSessions = new Map<string, Set<AgentSession>>();
protected pausedAborted = new Set<string>();
protected pausedAbortProvenance = new Map<string, PausedAbortProvenance>();
protected completionFinalizedTaskIds = new Set<string>();
protected depAborted = new Set<string>();
protected stuckAborted = new Map<string, boolean>();
protected userCanceledTaskIds = new Set<string>();
protected graphExecuteSelfRequeued = new Set<string>();
protected loopRecoveryState = new Map<string, { attempts: number; pending: boolean }>();
protected spawnedAgents = new Map<string, Set<string>>();
protected tokenUsageBaselines = new Map<string, { inputTokens: number; outputTokens: number; cachedTokens: number; cacheWriteTokens: number; totalTokens: number }>();
protected branchConflictErrorCount = new Map<string, number>();
protected completedTaskWatchdogs = new Map<string, ReturnType<typeof setTimeout>>();
protected workflowRerunWatchdogs = new Map<string, ReturnType<typeof setTimeout>>();
protected pendingEphemeralDeletions = new Set<string>();
protected workspaceConfig: WorkspaceConfig | null | undefined = undefined;
/** Workspace mode is memoized per executor host; a real settings transition must refresh it live. */
public invalidateWorkspaceConfig(): void {
this.workspaceConfig = undefined;
invalidateWorkspaceConfigCache(this);
}
protected childSessions = new Map<string, AgentSession>();
protected totalSpawnedCount = 0;
protected tokenCapDetector = new TokenCapDetector();
protected _modelRegistry?: Promise<ModelRegistry>;
protected _approvalRequestStore?: ApprovalRequestStore;
protected currentRunContexts = new Map<string, RunMutationContext>();
protected outerConcurrencyClaims = new Set<string>();
protected graphToolFailureRunCursors = new Map<string, number>();
protected graphStepSessionPinned = new Set<string>();
protected graphStepRunOnce = new Map<string, Promise<{ taskDone: boolean; modifiedFiles: string[]; exit?: ImplementationExit }>>();
protected graphStepActiveContext = new Map<string, ForeachActiveContext>();
protected graphRethinkNarrations = new Map<string, string>();
protected graphColumnAgentResolver = new Map<string, (nodeId: string) => WorkflowColumnAgent | undefined>();
protected graphUnattendedRuns = new Set<string>();
protected graphSeamGoverningNodeId = new Map<string, string>();
protected graphSeamThinkingLevel = new Map<string, ThinkingLevel>();
protected graphSeamSkillName = new Map<string, string>();
protected mergeRequester?: (taskId: string, options?: { signal?: AbortSignal }) => Promise<MergeResult>;
protected sessionContentionHoldAttempts = new Map<string, number>();
/**
* FNXC:CodeOrganization 2026-08-04-07:40:
* Process-wide graph-routing set lives on the state base (U4). Instance getter keeps
* host.graphRouting / host.constructor.processWideGraphRouting bag access unchanged.
*/
protected static processWideGraphRouting = new Set<string>();
protected get graphRouting(): Set<string> {
return (this.constructor as typeof TaskExecutorState).processWideGraphRouting;
}
}