import { createHash, randomUUID } from "node:crypto"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { checkpointGoalActiveTime, formatDuration, formatTokenCount, updateGoalUsage } from "./accounting.js"; import { findFinalAssistantMessage, formatError, isStaleContextError, isUsageLimitedGoalInterruption, notifyTerminal, safeGoalMenuText, truncateNotification, } from "./errors.js"; import { createGoalContextContract, createInactiveGoalContextContract, hasGoalContextContract, hasGoalContextContractHistory, hasGoalContractPublication, hasInactiveGoalContextContract, } from "./goal-contract.js"; import { appendGoalPromptMarker, extractContinuationMarker, extractGoalPromptMarker } from "./markers.js"; import { type ActiveGoal, clearLegacyPersistedGoal, type LegacyQueueState, type SafetyPauseCause, serializeGoalState, } from "./persistence.js"; import { buildContinuePrompt, type GoalStatus } from "./prompts.js"; import { nextToolFreeRepeatState, resetGoalSafetyEpoch } from "./safety.js"; export { queueGoalSafetyReset, resetGoalSafetyEpoch } from "./safety.js"; import { DEFAULT_GOAL_SETTINGS, type GoalSettings, type GoalSettingsLoadIssue } from "./settings.js"; import { assertGoalToolsAvailable, goalToolsAvailable } from "./tool-policy.js"; import { UsageLimitRecovery } from "./usage-limit-recovery.js"; import { type GoalWait, GoalWaitTimer, type UsageLimitScope } from "./wait.js"; import { WorkflowMutex, type WorkflowMutexOwner } from "./workflow-mutex.js"; export { GOAL_BLOCKED_TOOL, GOAL_COMPLETE_TOOL, GOAL_TOOL_NAMES, GOAL_WAIT_TOOL, } from "./tool-policy.js"; export interface ContinuationTicket { goalId: string; iteration: number; marker: string; prompt: string; usageLimitRetry?: UsageLimitScope; } export interface BudgetWrapUp { goalId: string; delivered: boolean; } export type GoalRecoveryKind = "provider_retry" | "provider_error" | "compaction_retry"; export type GoalRunOrigin = "manual" | "automatic"; export interface GoalRecovery { goalId: string; kind: GoalRecoveryKind; automaticOwner: boolean; errorMessage?: string; usageLimitScope?: UsageLimitScope; } export interface CompletedGoalRun { goalId?: string | null; origin?: GoalRunOrigin; toolAttempted: boolean; usageLimitRetry?: UsageLimitScope; } type StoppedGoalStatus = "paused" | "blocked" | "usage_limited" | "budget_limited"; export type GoalStopRequest = | { kind: "explicit_pause"; expectedGoalId: string } | { kind: "budget_limit"; expectedGoalId: string; reason: string } | { kind: "safety_pause"; expectedGoalId: string; cause: SafetyPauseCause; abortTurn: boolean; reason: string; } | { kind: "recovery_ended"; expectedGoalId: string; status: "paused" | "blocked" | "usage_limited"; reason: string; } | { kind: "tools_unavailable"; expectedGoalId: string; abortTurn: boolean; recordUsage: boolean; } | { kind: "blocker_report"; expectedGoalId: string; reason: string } | { kind: "agent_interruption"; expectedGoalId: string; status: "paused" | "blocked" | "usage_limited"; reason: string; } | { kind: "activation_rollback"; expectedGoalId: string; restoreGoal: ActiveGoal; abortTurn: boolean; }; export interface StatusContext { cwd: string; mode?: "tui" | "rpc" | "json" | "print"; ui: { confirm: (title: string, message: string) => Promise; notify: (message: string, level?: "info" | "warning" | "error") => void; setStatus: (key: string, value: string | undefined) => void; }; isIdle?: () => boolean; hasPendingMessages?: () => boolean; abort?: () => void; sessionManager?: unknown; model?: ExtensionContext["model"]; } export const STATUS_KEY = "goal"; export const GOAL_STATE_ENTRY_TYPE = "goal-state"; export const MAX_GOAL_ID_LENGTH = 128; /** Canonical Goal state passed to the in-process managed-run publisher. */ export type GoalStateSnapshotStatus = GoalStatus | "cleared"; export interface GoalStateSnapshot { goalId: string; status: GoalStateSnapshotStatus; summary?: string; reason?: string; } /** Terminal statuses for Goal persistence and managed-run lifecycle publication. */ export function isTerminalGoalStatus(status: GoalStateSnapshotStatus): boolean { return status !== "active"; } function buildGoalStateSnapshot( goal: ActiveGoal, summary: string | undefined, reason: string | undefined, ): GoalStateSnapshot { const snapshot: GoalStateSnapshot = { goalId: goal.id, status: goal.status }; if (goal.status === "complete" && summary) snapshot.summary = summary; else if (goal.status !== "complete" && isTerminalGoalStatus(goal.status) && reason) { snapshot.reason = reason; } return snapshot; } interface GoalTerminalDetails { goalId: string; summary?: string; reason?: string; } export interface GoalSettingsRuntimeSnapshot { settings: GoalSettings; activeGoal?: ActiveGoal; legacyQueueState?: LegacyQueueState; legacyExperimentalGoalsSetting: boolean; continuationIntent?: ContinuationTicket; continuationDelivery?: ContinuationTicket; goalRecovery?: GoalRecovery; budgetWrapUp?: BudgetWrapUp; pendingLengthRecovery?: GoalRecovery; guardAbortGoalId?: string; staleGoalToolCallsBlocked: boolean; cancelledContinuationMarkers: [string, string][]; terminalDetails?: GoalTerminalDetails; } type GoalContextContract = | ReturnType | ReturnType; interface PendingGoalContract { content: string; publicationId: string; } interface PendingGoalPrompt { goalId: string; resetSafetyEpoch: boolean; fingerprint: string; prompt: string; } interface PendingNonGoalInput { behavior: "steer" | "followUp"; fingerprint: string; resetSafetyEpoch: boolean; } const MAX_CANCELLED_CONTINUATION_PROMPTS = 20; const MAX_PENDING_GOAL_PROMPTS = 20; const MAX_PENDING_NON_GOAL_INPUTS = 20; const BUDGET_WRAP_UP_MESSAGE_TYPE = "goal-budget-wrap-up"; const BUDGET_WRAP_UP_PROMPT = "The active /goal token budget is exhausted. Stop substantive work and do not call substantive tools. Summarize progress, verified results, remaining work, and blockers concisely. Treat completion as unproven. Do not call goal_complete unless authoritative, requirement-by-requirement evidence already proves every requirement is complete. Weak, indirect, or missing evidence is not enough. Budget exhaustion is not completion."; const CONTRADICTORY_COMPLETION_PATTERNS = [ /(? void; private settingsStatePublications?: GoalStateSnapshot[]; private settingsCommitEffects?: Array<() => void>; readonly usageLimitRecovery: UsageLimitRecovery; legacyQueueState?: LegacyQueueState; legacyExperimentalGoalsSetting = false; completionStatusTimer?: NodeJS.Timeout; private continuationDispatchTimer?: NodeJS.Timeout; private readonly goalWaitTimer = new GoalWaitTimer(); private goalWaitDeadlineRetry?: { goalId: string; resumeAt: number; retryAt?: number; exhausted: boolean; }; continuationIntent?: ContinuationTicket; continuationDelivery?: ContinuationTicket; goalRecovery?: GoalRecovery; // Unconfirmed length recovery is invalidated by the same runtime transitions // as provider errors, including a later accepted goal_wait. pendingLengthRecovery?: GoalRecovery; budgetWrapUp?: BudgetWrapUp; /** `null` marks a run that must not be charged to the active goal. */ agentRunGoalId?: string | null; agentRunOrigin?: GoalRunOrigin; agentRunToolAttempted = false; agentRunUsageLimitRetry?: UsageLimitScope; private agentRunResponseSeen = false; guardAbortGoalId?: string; staleGoalToolCallsBlocked = false; private readonly workflowMutex: WorkflowMutex; private workflowOwner?: WorkflowMutexOwner; private workflowSession?: object; private workflowSessionId?: unknown; private pendingGoalContract?: PendingGoalContract; pendingGoalPromptMarkers = new Map(); claimedGoalPromptMarkers = new Map(); cancelledGoalPromptMarkers = new Map(); cancelledContinuationMarkers = new Map(); claimedContinuationMarkers = new Map(); pendingNonGoalInputs: PendingNonGoalInput[] = []; menuGeneration = 0; menuController = new AbortController(); readonly pi: ExtensionAPI; constructor(pi: ExtensionAPI) { this.pi = pi; this.workflowMutex = new WorkflowMutex(pi); this.usageLimitRecovery = new UsageLimitRecovery(this); } goalToolsAvailable() { return goalToolsAvailable(this.pi); } assertGoalToolsAvailable() { assertGoalToolsAvailable(this.pi); } bindWorkflowSession(session: object) { this.pendingGoalContract = undefined; this.workflowSession = session; this.workflowSessionId = goalWaitSessionId(session); this.workflowOwner = undefined; this.workflowMutex.bindSession(session); } unbindWorkflowSession(session: object) { if (this.workflowSession !== session) return; this.pendingGoalContract = undefined; this.workflowMutex.unbindSession(session); this.workflowOwner = undefined; this.workflowSession = undefined; this.workflowSessionId = undefined; } acquireWorkflow(session?: unknown) { if ( session && typeof session === "object" && (this.workflowSession !== session || this.workflowSessionId !== goalWaitSessionId(session)) ) { this.bindWorkflowSession(session); } if (this.workflowMutex.isOwner(this.workflowOwner)) return true; const owner = this.workflowMutex.acquire(); if (!owner) return false; this.workflowOwner = owner; return true; } ownsWorkflow(goal: ActiveGoal | undefined = this.activeGoal) { try { return ( goal?.status === "active" && this.workflowMutex.isOwner(this.workflowOwner) && this.workflowSessionId === goalWaitSessionId(this.workflowSession) ); } catch { return false; } } releaseWorkflow() { const owner = this.workflowOwner; this.workflowMutex.release(owner); if (!this.workflowMutex.isOwner(owner)) this.workflowOwner = undefined; } hasLegacyQueueInterface() { return this.legacyExperimentalGoalsSetting || this.legacyQueueState !== undefined; } setGoalStateSink(sink: ((snapshot: GoalStateSnapshot) => void) | undefined) { this.goalStateSink = sink; } private publishGoalState(snapshot: GoalStateSnapshot) { if (this.settingsStatePublications) { this.settingsStatePublications.push(snapshot); return; } try { this.goalStateSink?.(snapshot); } catch { // Protocol publication must not interrupt canonical Goal persistence. } } /** Publish managed-run transitions only after the settings transaction commits. */ withSettingsPublication(apply: () => void) { const previous = this.settingsStatePublications; const previousEffects = this.settingsCommitEffects; const pending: GoalStateSnapshot[] = []; const effects: Array<() => void> = []; this.settingsStatePublications = pending; this.settingsCommitEffects = effects; try { apply(); } catch (error) { this.settingsStatePublications = previous; this.settingsCommitEffects = previousEffects; throw error; } this.settingsStatePublications = previous; this.settingsCommitEffects = previousEffects; if (previousEffects) previousEffects.push(...effects); else for (const effect of effects) effect(); for (const snapshot of pending) this.publishGoalState(snapshot); } private afterSettingsCommit(ctx: StatusContext, goalId: string, effect: () => void) { if (!this.settingsCommitEffects) { effect(); return; } this.settingsCommitEffects.push(() => { if (this.activeGoal?.id !== goalId) return; try { effect(); } catch (error) { notifyWhenSessionAlive(ctx, `Goal settings saved, but cleanup failed: ${formatError(error)}`, "error"); } }); } replaceMenuSession() { this.usageLimitRecovery.cancel(); this.menuGeneration += 1; this.menuController.abort(new DOMException("Goal session replaced", "AbortError")); this.menuController = new AbortController(); } closeMenuSession() { this.usageLimitRecovery.cancel(); this.menuGeneration += 1; this.menuController.abort(new DOMException("Goal session shut down", "AbortError")); } canRecordGoalUsage(goalId?: string) { return ( this.agentRunGoalId !== null && (goalId === undefined || this.agentRunGoalId === undefined || this.agentRunGoalId === goalId) ); } hasActiveBudgetWrapUp() { return ( this.activeGoal?.status === "budget_limited" && this.budgetWrapUp?.goalId === this.activeGoal.id && this.budgetWrapUp.delivered ); } hasActiveGoalRecovery() { return Boolean(this.activeGoal && this.goalRecovery?.goalId === this.activeGoal.id); } beginAgentRun( goalId: string | null | undefined, origin: GoalRunOrigin | undefined, usageLimitRetry?: UsageLimitScope, ) { this.agentRunGoalId = goalId; this.agentRunOrigin = origin; this.agentRunToolAttempted = false; this.agentRunUsageLimitRetry = usageLimitRetry; this.agentRunResponseSeen = false; } beginRecoveryRunIfNeeded() { if (this.agentRunGoalId !== undefined || !this.activeGoal) return; const recovery = this.goalRecovery; if (!recovery || recovery.goalId !== this.activeGoal.id) return; this.beginAgentRun(recovery.goalId, recovery.automaticOwner ? "automatic" : "manual"); } markAgentToolAttempted() { if (this.agentRunGoalId !== undefined) this.agentRunToolAttempted = true; } finishAgentRun(): CompletedGoalRun { const run = { goalId: this.agentRunGoalId, origin: this.agentRunOrigin, toolAttempted: this.agentRunToolAttempted, usageLimitRetry: this.agentRunUsageLimitRetry, }; this.clearAgentRun(); return run; } clearAgentRun() { this.agentRunGoalId = undefined; this.agentRunOrigin = undefined; this.agentRunToolAttempted = false; this.agentRunUsageLimitRetry = undefined; this.agentRunResponseSeen = false; } reclassifyAgentRunAsManual() { if (this.agentRunGoalId !== undefined) this.agentRunOrigin = "manual"; } isAutomaticRunForGoal(goalId: string) { return this.agentRunGoalId === goalId && this.agentRunOrigin === "automatic"; } recordGoalUsage( goal: ActiveGoal, ctx: StatusContext, checkpointActiveTime = goal.status === "active" && !goal.waiting, ) { if (!this.canRecordGoalUsage(goal.id)) return false; updateGoalUsage(goal, ctx, checkpointActiveTime); return true; } requestContinuation(goal: ActiveGoal, usageLimitRetry?: UsageLimitScope) { if (!this.ownsWorkflow(goal)) return false; if (goal.waiting || this.hasContinuationWorkForGoal(goal.id)) return false; const marker = continuationMarker(goal); this.continuationIntent = { goalId: goal.id, iteration: goal.iteration, marker, prompt: buildContinuePrompt(goal, marker), ...(usageLimitRetry ? { usageLimitRetry } : {}), }; return true; } dispatchContinuationIfSettled(ctx: StatusContext) { const intent = this.continuationIntent; if (!this.ownsWorkflow() || (this.workflowSession && ctx.sessionManager !== this.workflowSession)) { this.cancelContinuationWork(); return false; } if (!intent) return false; if (this.activeGoal?.status === "active" && !this.goalToolsAvailable()) { this.pauseGoalForUnavailableTools(ctx); return false; } if ( !this.activeGoal || this.activeGoal.id !== intent.goalId || this.activeGoal.status !== "active" || this.activeGoal.waiting ) { this.continuationIntent = undefined; return false; } if ( this.limitActiveGoalForBudget(ctx, false) || this.enforceAutomaticTurnLimit(ctx, false) || this.enforceNoProgressLimit(ctx) ) { return false; } if (ctx.isIdle?.() !== true || hasPendingMessages(ctx)) return false; this.clearContinuationDispatchTimer(); this.continuationIntent = undefined; this.continuationDelivery = intent; try { this.pi.sendUserMessage(intent.prompt, { deliverAs: "followUp" }); return true; } catch (error) { if (this.continuationDelivery?.marker === intent.marker) { this.continuationDelivery = undefined; } if (this.activeGoal?.id === intent.goalId && this.activeGoal.status === "active") { this.continuationIntent = intent; } notifyWhenSessionAlive(ctx, `Goal prompt failed: ${formatError(error)}`, "error"); return false; } } hasContinuationWorkForGoal(goalId: string) { return this.continuationIntent?.goalId === goalId || this.continuationDelivery?.goalId === goalId; } enterGoalWait(ctx: StatusContext, goalId: string, waiting: GoalWait) { const goal = this.activeGoal; if (!goal || goal.id !== goalId || !this.ownsWorkflow(goal)) return undefined; this.recordGoalUsage(goal, ctx, false); this.cancelContinuationWork(); this.clearGoalRecoveryForGoal(goal.id); this.clearGoalWaitTimer(); this.activeGoal = { ...goal, waiting, activeStartedAt: undefined, updatedAt: Date.now(), }; const waitingGoal = this.activeGoal; this.persistGoal(waitingGoal); if (this.activeGoal !== waitingGoal || !this.ownsWorkflow(waitingGoal)) return undefined; this.updateStatus(ctx, waitingGoal); if (this.activeGoal !== waitingGoal || !this.ownsWorkflow(waitingGoal)) return undefined; this.restoreGoalWaitTimer(ctx); return waitingGoal; } clearGoalWait(ctx: StatusContext, goalId: string) { const goal = this.activeGoal; if (!goal || goal.id !== goalId || !this.ownsWorkflow(goal) || !goal.waiting) return false; this.clearGoalWaitTimer(); const { waiting: _waiting, ...nextGoal } = goal; const now = Date.now(); checkpointGoalActiveTime(nextGoal, now, true); nextGoal.updatedAt = now; this.activeGoal = nextGoal; this.persistGoal(nextGoal); if (this.activeGoal !== nextGoal || !this.ownsWorkflow(nextGoal)) return false; this.updateStatus(ctx, nextGoal); return this.activeGoal === nextGoal && this.ownsWorkflow(nextGoal); } restoreGoalWaitTimer(ctx: StatusContext) { this.clearGoalWaitTimer(); const goal = this.activeGoal; if (!this.ownsWorkflow(goal)) return false; const resumeAt = goal?.status === "active" ? goal.waiting?.resumeAt : undefined; if (!goal || resumeAt === undefined) return false; if (this.usageLimitRecovery.stopIfDisabled(ctx)) return false; if (goal.waiting && !this.usageLimitRecovery.matches(ctx, goal.waiting)) { this.usageLimitRecovery.modelChanged(ctx); return false; } this.scheduleGoalWaitTimer(ctx, goal.id, resumeAt); return true; } dispatchDueGoalWait(ctx: StatusContext) { const goal = this.activeGoal; if (!this.ownsWorkflow(goal) || (this.workflowSession && ctx.sessionManager !== this.workflowSession)) return false; const waiting = goal?.status === "active" ? goal.waiting : undefined; const resumeAt = waiting?.resumeAt; if (!goal || !waiting || resumeAt === undefined) return false; if (this.usageLimitRecovery.stopIfDisabled(ctx)) return false; if (!this.usageLimitRecovery.matches(ctx, waiting)) { this.usageLimitRecovery.modelChanged(ctx); return false; } const retry = this.goalWaitDeadlineRetry; const matchingRetry = retry?.goalId === goal.id && retry.resumeAt === resumeAt ? retry : undefined; if (matchingRetry?.exhausted) return false; const wakeAt = matchingRetry?.retryAt ?? resumeAt; if (Date.now() < wakeAt) return false; const retryAttempt = matchingRetry?.retryAt !== undefined; if (ctx.isIdle?.() !== true || hasPendingMessages(ctx)) return false; if (retryAttempt) this.goalWaitDeadlineRetry = undefined; if (!this.clearGoalWait(ctx, goal.id)) return false; const resumedGoal = this.activeGoal; if (!resumedGoal || resumedGoal.id !== goal.id || resumedGoal.status !== "active") return false; this.requestContinuation(resumedGoal, waiting.usageLimit); const dispatched = this.dispatchContinuationIfSettled(ctx); if (dispatched) return true; if ( this.activeGoal?.id === goal.id && this.activeGoal.status === "active" && this.continuationIntent?.goalId === goal.id ) { this.restoreGoalWaitAfterDeadlineFailure(ctx, goal.id, waiting, !retryAttempt); } return false; } clearGoalWaitTimer() { this.usageLimitRecovery.cancel(); this.goalWaitTimer.clear(); this.goalWaitDeadlineRetry = undefined; } private scheduleGoalWaitTimer(ctx: StatusContext, goalId: string, wakeAt: number) { const generation = this.menuGeneration; const session = ctx.sessionManager; const sessionId = goalWaitSessionId(session); this.goalWaitTimer.schedule(wakeAt, () => { if (generation !== this.menuGeneration || this.activeGoal?.id !== goalId || !this.ownsWorkflow(this.activeGoal)) { return; } try { if (ctx.sessionManager !== session || goalWaitSessionId(session) !== sessionId) return; this.dispatchDueGoalWait(ctx); } catch (error) { notifyWhenSessionAlive(ctx, `Goal wait deadline failed: ${formatError(error)}`, "error"); } }); } private restoreGoalWaitAfterDeadlineFailure( ctx: StatusContext, goalId: string, waiting: GoalWait, allowRetry: boolean, ) { const goal = this.activeGoal; if (!goal || goal.id !== goalId || goal.status !== "active" || waiting.resumeAt === undefined) { return; } this.cancelContinuationWork(); this.recordGoalUsage(goal, ctx, false); this.goalWaitTimer.clear(); this.activeGoal = { ...goal, waiting, activeStartedAt: undefined, updatedAt: Date.now(), }; const retryAt = allowRetry ? Date.now() + 1_000 : undefined; this.goalWaitDeadlineRetry = { goalId, resumeAt: waiting.resumeAt, retryAt, exhausted: !allowRetry, }; this.persistGoal(this.activeGoal); this.updateStatus(ctx, this.activeGoal); if (retryAt !== undefined) this.scheduleGoalWaitTimer(ctx, goalId, retryAt); } updateStatus(ctx: StatusContext, goal: ActiveGoal) { this.clearCompletionStatusTimer(); ctx.ui.setStatus(STATUS_KEY, formatStatus(goal, this.settings.continuationLimits.automaticTurns)); } stopActiveGoal(ctx: StatusContext, request: GoalStopRequest) { const currentGoal = this.activeGoal; if (!currentGoal || currentGoal.id !== request.expectedGoalId) return undefined; this.clearGoalWaitTimer(); let goal = currentGoal; let status: StoppedGoalStatus; let terminalReason: string | undefined; switch (request.kind) { case "explicit_pause": this.recordGoalUsage(goal, ctx); this.cancelContinuationWork(); this.clearGoalRecoveryForGoal(goal.id); this.clearBudgetWrapUp(); this.blockStaleGoalToolCalls(); this.afterSettingsCommit(ctx, goal.id, () => abortCurrentTurn(ctx)); status = "paused"; break; case "budget_limit": this.cancelContinuationWork(); this.clearGoalRecoveryForGoal(goal.id); this.clearBudgetWrapUp(); this.blockStaleGoalToolCalls(); this.afterSettingsCommit(ctx, goal.id, () => abortCurrentTurn(ctx)); status = "budget_limited"; terminalReason = request.reason; break; case "safety_pause": this.cancelContinuationWork(); this.clearGoalRecoveryForGoal(goal.id); this.clearBudgetWrapUp(); this.blockStaleGoalToolCalls(); if (request.abortTurn) { this.guardAbortGoalId = goal.id; this.afterSettingsCommit(ctx, goal.id, () => abortCurrentTurn(ctx)); } goal = { ...goal, safetyPauseCause: request.cause }; status = "paused"; terminalReason = request.reason; break; case "recovery_ended": this.clearGoalRecoveryForGoal(goal.id); this.cancelContinuationWork(); this.clearBudgetWrapUp(); this.blockStaleGoalToolCalls(); status = request.status; terminalReason = request.reason; break; case "tools_unavailable": if (request.recordUsage) this.recordGoalUsage(goal, ctx); this.cancelContinuationWork(); this.clearGoalRecoveryForGoal(goal.id); this.clearBudgetWrapUp(); if (request.abortTurn) { this.blockStaleGoalToolCalls(); this.afterSettingsCommit(ctx, goal.id, () => abortCurrentTurn(ctx)); } else { this.clearStaleGoalToolCallBlock(); } status = "paused"; break; case "blocker_report": this.recordGoalUsage(goal, ctx); this.cancelContinuationWork(); this.clearBudgetWrapUp(); this.clearGoalRecoveryForGoal(goal.id); this.blockStaleGoalToolCalls(); status = "blocked"; terminalReason = request.reason; break; case "agent_interruption": this.cancelContinuationWork(); this.clearBudgetWrapUp(); this.blockStaleGoalToolCalls(); this.afterSettingsCommit(ctx, goal.id, () => abortCurrentTurn(ctx)); status = request.status; terminalReason = request.reason; break; case "activation_rollback": goal = request.restoreGoal; if (request.abortTurn) this.afterSettingsCommit(ctx, goal.id, () => abortCurrentTurn(ctx)); this.blockStaleGoalToolCalls(); status = "paused"; break; } this.activeGoal = transitionGoal(goal, status); if (terminalReason !== undefined) this.setTerminalReason(this.activeGoal.id, terminalReason); const stoppedGoal = this.activeGoal; this.persistGoal(stoppedGoal); if (request.kind !== "blocker_report") { this.afterSettingsCommit(ctx, stoppedGoal.id, () => { if (this.activeGoal?.status !== "active") this.ensureInactiveGoalContextContract(ctx); }); } if (this.activeGoal?.id === stoppedGoal.id && this.activeGoal.status === stoppedGoal.status) { this.updateStatus(ctx, stoppedGoal); this.afterSettingsCommit(ctx, stoppedGoal.id, () => { if (this.activeGoal?.status === stoppedGoal.status) this.releaseWorkflow(); }); } return stoppedGoal; } blockStaleGoalToolCalls() { this.staleGoalToolCallsBlocked = true; } clearStaleGoalToolCallBlock() { this.staleGoalToolCallsBlocked = false; } clearGoalRecovery() { this.goalRecovery = undefined; this.pendingLengthRecovery = undefined; } clearBudgetWrapUp() { this.budgetWrapUp = undefined; } setCompletionSummary(goalId: string, summary: string) { this.terminalDetails = { goalId, summary }; } setTerminalReason(goalId: string, reason: string) { this.terminalDetails = { goalId, reason }; } clearTerminalDetails() { this.terminalDetails = undefined; } isActiveBudgetWrapUpMessage(message: unknown) { if (!message || typeof message !== "object") return false; const candidate = message as { role?: unknown; customType?: unknown; details?: { goalId?: unknown }; }; return ( candidate.role === "custom" && candidate.customType === BUDGET_WRAP_UP_MESSAGE_TYPE && typeof candidate.details?.goalId === "string" && candidate.details.goalId === this.budgetWrapUp?.goalId && candidate.details.goalId === this.activeGoal?.id ); } keepBudgetWrapUpMessage(message: unknown) { if (!message || typeof message !== "object") return true; const candidate = message as { role?: unknown; customType?: unknown }; if (candidate.role !== "custom" || candidate.customType !== BUDGET_WRAP_UP_MESSAGE_TYPE) { return true; } return this.isActiveBudgetWrapUpMessage(message); } queueBudgetWrapUp(ctx: StatusContext, goal: ActiveGoal) { if (!this.ownsWorkflow(goal)) return false; if (!this.budgetWrapUp || this.budgetWrapUp.goalId !== goal.id) { this.budgetWrapUp = { goalId: goal.id, delivered: false }; } if (this.budgetWrapUp.delivered) return true; this.budgetWrapUp.delivered = true; try { this.pi.sendMessage( { customType: BUDGET_WRAP_UP_MESSAGE_TYPE, content: BUDGET_WRAP_UP_PROMPT, display: true, details: { goalId: goal.id }, }, { deliverAs: "steer" }, ); return true; } catch (error) { this.budgetWrapUp.delivered = false; notifyWhenSessionAlive(ctx, `Goal budget wrap-up failed: ${formatError(error)}`, "error"); return false; } } limitActiveGoalForBudget(ctx: StatusContext, sendWrapUp: boolean) { const goal = this.activeGoal; if (goal?.status !== "active" || goal.tokenBudget === undefined || goal.tokensUsed < goal.tokenBudget) { return false; } const stoppedGoal = this.stopActiveGoal(ctx, { kind: "budget_limit", expectedGoalId: goal.id, reason: `token budget reached (${formatBudget(goal)})`, }); if (!stoppedGoal) return false; notifyTerminal(ctx.ui, `Goal token budget reached: ${formatBudget(stoppedGoal)}`, "warning"); if (sendWrapUp) this.queueBudgetWrapUp(ctx, stoppedGoal); return true; } recordAutomaticTurn(ctx: StatusContext, message: unknown) { const goal = this.activeGoal; if (goal?.status !== "active") return false; const candidate = findFinalAssistantMessage([message]); if ( this.agentRunGoalId === goal.id && candidate && candidate.stopReason !== "error" && candidate.stopReason !== "aborted" ) { // A successful native response has recovered, even in a manual-origin run // or a tool loop that has not reached agent_end yet. Keep native ownership, // but retire the quota-policy scope before settings or model events run. if (this.goalRecovery?.goalId === goal.id) this.goalRecovery.usageLimitScope = undefined; this.agentRunUsageLimitRetry = undefined; } if (!this.isAutomaticRunForGoal(goal.id)) return false; if (candidate?.stopReason === "aborted") return false; const quotaRejected = this.agentRunUsageLimitRetry !== undefined && !this.agentRunResponseSeen && !this.agentRunToolAttempted && candidate && isUsageLimitedGoalInterruption(candidate) && (candidate.content ?? []).every((block) => block.type === "text" && block.text.trim() === ""); this.agentRunResponseSeen = true; if (!quotaRejected) { this.agentRunUsageLimitRetry = undefined; goal.automaticModelTurns = Math.min(Number.MAX_SAFE_INTEGER, goal.automaticModelTurns + 1); } this.recordGoalUsage(goal, ctx); this.persistGoal(goal); this.updateStatus(ctx, goal); // Terminal errors need agent_end classification before a safety pause can // choose between usage_limited, blocked, or retryable cleanup. if (candidate?.role === "assistant" && candidate.stopReason === "error") return false; return this.enforceAutomaticTurnLimit(ctx, true); } recordAutomaticRunProgress(ctx: StatusContext, goalId: string, messages: readonly unknown[], toolAttempted: boolean) { const goal = this.activeGoal; if (goal?.id !== goalId || goal.status !== "active") return false; const next = nextToolFreeRepeatState(goal, messages, toolAttempted); goal.toolFreeRepeatCount = next.toolFreeRepeatCount; goal.lastToolFreeOutputFingerprint = next.lastToolFreeOutputFingerprint; this.persistGoal(goal); const currentGoal = this.activeGoal; if (currentGoal?.id !== goalId || !this.ownsWorkflow(currentGoal)) return true; this.updateStatus(ctx, currentGoal); const limit = this.settings.continuationLimits.noProgressTurns; if (limit === null || currentGoal.toolFreeRepeatCount < limit) return false; return this.pauseGoalForSafety(ctx, "no_progress", false); } enforceAutomaticTurnLimit(ctx: StatusContext, abortTurn: boolean) { const goal = this.activeGoal; const limit = this.settings.continuationLimits.automaticTurns; if (goal?.status !== "active" || limit === null || goal.automaticModelTurns < limit) { return false; } return this.pauseGoalForSafety(ctx, "continuation_limit", abortTurn); } enforceNoProgressLimit(ctx: StatusContext, abortTurn = false) { const goal = this.activeGoal; const limit = this.settings.continuationLimits.noProgressTurns; if (goal?.status !== "active" || limit === null || goal.toolFreeRepeatCount < limit) { return false; } return this.pauseGoalForSafety(ctx, "no_progress", abortTurn); } pauseGoalForSafety(ctx: StatusContext, cause: SafetyPauseCause, abortTurn: boolean) { const goal = this.activeGoal; if (goal?.status !== "active") return false; const automaticLimit = this.settings.continuationLimits.automaticTurns; const count = cause === "continuation_limit" ? `${goal.automaticModelTurns} of ${automaticLimit ?? "Unlimited"} automatic model responses` : `no progress across ${goal.toolFreeRepeatCount} automatic runs`; const stoppedGoal = this.stopActiveGoal(ctx, { kind: "safety_pause", expectedGoalId: goal.id, cause, abortTurn, reason: `${cause} (${count}; ${formatTokenCount(goal.tokensUsed)} tokens)`, }); if (!stoppedGoal) return false; notifyTerminal( ctx.ui, cause === "continuation_limit" ? `Automatic-work limit reached: ${stoppedGoal.automaticModelTurns} of ${automaticLimit} responses. Goal progress is saved with ${formatTokenCount(stoppedGoal.tokensUsed)} cumulative tokens. Open /goal to review and continue.` : `Goal paused: ${count}; ${formatTokenCount(stoppedGoal.tokensUsed)} cumulative tokens. Open /goal to review and continue.`, "warning", ); return true; } resetActiveSafetyEpoch(ctx: StatusContext) { const goal = this.activeGoal; if (goal?.status !== "active") return false; this.activeGoal = resetGoalSafetyEpoch(goal); this.reclassifyAgentRunAsManual(); this.persistGoal(this.activeGoal); this.updateStatus(ctx, this.activeGoal); return true; } finalizeSettledRecovery(ctx: StatusContext, aborted: boolean, settledGoalId: string | undefined) { const recovery = this.goalRecovery; if (!recovery && !aborted) return false; this.goalRecovery = undefined; const goal = this.activeGoal; if (!goal || goal.id !== (recovery?.goalId ?? settledGoalId) || !this.ownsWorkflow(goal)) { return false; } const details = recovery?.errorMessage ? `: ${truncateNotification(recovery.errorMessage)}` : ""; const usageLimited = isUsageLimitedGoalInterruption({ role: "assistant", stopReason: "error", errorMessage: recovery?.errorMessage, }); if ( !aborted && usageLimited && this.settings.retryUsageLimits && this.usageLimitRecovery.enter(ctx, recovery?.errorMessage, recovery?.usageLimitScope) ) return true; if (!aborted && !usageLimited && recovery && recovery.kind !== "compaction_retry") { // Native post-run work can change limits or tool availability after agent_end. if ( this.enforceAutomaticTurnLimit(ctx, false) || this.enforceNoProgressLimit(ctx) || this.limitActiveGoalForBudget(ctx, false) ) { return true; } if (!this.goalToolsAvailable()) return this.pauseGoalForUnavailableTools(ctx, false); // A parallel tool batch can continue after accepting goal_wait. Preserve // its arranged wake source and deadline instead of replacing that wait. if (goal.waiting) return true; const nativeRetry = recovery.kind === "provider_retry"; const waitingGoal = this.enterGoalWait(ctx, goal.id, { reason: `${nativeRetry ? "Provider retries exhausted" : "Provider error"}${details}`, }); if (!waitingGoal) return false; notifyTerminal( ctx.ui, nativeRetry ? `Goal waiting after provider retries were exhausted${details}. Send a follow-up or run /goal resume to retry.` : `Goal waiting after a provider error${details}. No automatic retry was added. Send a follow-up or run /goal resume to continue.`, "warning", ); return true; } // Cancellation during backoff or after agent_end need not produce another // assistant or compaction event. Keep the ended Goal identity as a fallback. const status = aborted ? "paused" : usageLimited ? "usage_limited" : "blocked"; const stoppedGoal = this.stopActiveGoal(ctx, { kind: "recovery_ended", expectedGoalId: goal.id, status, reason: aborted ? "goal paused after agent interruption" : `agent error after retries${details}`, }); if (!stoppedGoal) return false; const message = aborted ? "Goal paused after interruption. Run /goal resume to continue." : usageLimited ? `Goal stopped after provider usage limit${details}. Run /goal resume when usage is available.` : `Goal blocked after agent error retries were exhausted${details}. Resolve the blocker or run /goal resume to retry.`; notifyTerminal(ctx.ui, message, "warning"); return true; } clearSettledSafetyTracking() { this.guardAbortGoalId = undefined; this.pendingNonGoalInputs = []; this.claimedGoalPromptMarkers.clear(); this.claimedContinuationMarkers.clear(); this.clearAgentRun(); } clearGoalRecoveryForGoal(goalId: string) { if (this.goalRecovery?.goalId === goalId) this.goalRecovery = undefined; if (this.pendingLengthRecovery?.goalId === goalId) this.pendingLengthRecovery = undefined; } isPiOwnedCompactionRetry(event: unknown, goalId: string) { const compaction = event as { reason?: unknown; willRetry?: unknown }; if (compaction.willRetry === true) return true; return ( this.goalRecovery?.goalId === goalId && this.goalRecovery.kind === "compaction_retry" && (compaction.reason === undefined || compaction.reason === "overflow") ); } clearContinuationTracking() { this.clearContinuationDispatchTimer(); this.continuationIntent = undefined; this.continuationDelivery = undefined; this.cancelledContinuationMarkers.clear(); this.claimedContinuationMarkers.clear(); } clearPendingGoalPrompts() { this.pendingGoalPromptMarkers.clear(); this.claimedGoalPromptMarkers.clear(); this.cancelledGoalPromptMarkers.clear(); this.pendingNonGoalInputs = []; } ensureGoalContextContract(ctx: StatusContext, goal: ActiveGoal) { this.publishGoalContextContract(this.goalContextContractForPrompt(ctx, goal)); } ensureInactiveGoalContextContract(ctx: StatusContext) { this.publishGoalContextContract(this.goalContextContractForPrompt(ctx)); } private publishGoalContextContract(contract: GoalContextContract | undefined) { if (!contract) return; const previous = this.pendingGoalContract; const pending = { content: contract.content, publicationId: randomUUID() }; this.pendingGoalContract = pending; const message = { ...contract, details: { ...contract.details, publicationId: pending.publicationId } }; try { // Reserve before sending: a synchronous listener can re-enter publication. Pi does not // forward deferred custom-message events to extensions, so retire from saved entries. this.pi.sendMessage({ ...message, details: { ...message.details, sentAt: Date.now() } }, { triggerTurn: false }); } catch (error) { if (this.pendingGoalContract === pending) this.pendingGoalContract = previous; throw error; } } goalContextContractForPrompt(ctx: StatusContext, goal?: ActiveGoal) { const { contextEntries, historyEntries } = goalContractEntries(ctx); const pending = this.pendingGoalContract; if ( pending && (hasGoalContractPublication(contextEntries, pending.publicationId) || hasGoalContractPublication(historyEntries, pending.publicationId)) ) { this.pendingGoalContract = undefined; } const expected = goal ? createGoalContextContract(goal) : createInactiveGoalContextContract(); // The queued tail supersedes saved history: saved A -> queued inactive -> A needs a new A. if (this.pendingGoalContract) return this.pendingGoalContract.content === expected.content ? undefined : expected; if (goal) return hasGoalContextContract(contextEntries, goal) ? undefined : expected; const hasHistory = hasGoalContextContractHistory(contextEntries) || hasGoalContextContractHistory(historyEntries); return !hasHistory || hasInactiveGoalContextContract(contextEntries) ? undefined : expected; } hasGoalContextContractHistory(ctx: StatusContext) { const { contextEntries, historyEntries } = goalContractEntries(ctx); return hasGoalContextContractHistory(contextEntries) || hasGoalContextContractHistory(historyEntries); } async sendOwnedGoalPrompt( ctx: StatusContext, goalId: string, prompt: string, resetSafetyEpoch = true, isCurrent?: () => boolean, ) { if (this.activeGoal?.id !== goalId || !this.ownsWorkflow(this.activeGoal)) return false; const pending = this.rememberPendingGoalPrompt(goalId, prompt, resetSafetyEpoch); const sent = await sendPrompt(this.pi, ctx, pending.prompt, isCurrent); if (!sent || (isCurrent && !isCurrent()) || this.activeGoal?.id !== goalId || !this.ownsWorkflow(this.activeGoal)) { this.pendingGoalPromptMarkers.delete(pending.marker); return false; } // A busy agent queues the prompt as a followUp, so before_agent_start cannot persist the // contract until the current run ends; until then every request would append it at the tail. if (ctx.isIdle?.() === false) this.ensureGoalContextContract(ctx, this.activeGoal); return true; } cancelContinuationWork() { this.usageLimitRecovery.cancel(); this.clearContinuationDispatchTimer(); if (this.continuationDelivery) { this.rememberCancelledContinuationMarker(this.continuationDelivery); } this.continuationIntent = undefined; this.continuationDelivery = undefined; } scheduleContinuationDispatch(ctx: StatusContext, goalId: string) { this.clearContinuationDispatchTimer(); const generation = this.menuGeneration; this.continuationDispatchTimer = setTimeout(() => { this.continuationDispatchTimer = undefined; if (generation !== this.menuGeneration || this.activeGoal?.id !== goalId || !this.ownsWorkflow(this.activeGoal)) { return; } this.dispatchContinuationIfSettled(ctx); }, 0); } private clearContinuationDispatchTimer() { if (!this.continuationDispatchTimer) return; clearTimeout(this.continuationDispatchTimer); this.continuationDispatchTimer = undefined; } consumeCancelledGoalPrompt(prompt: string) { const marker = extractGoalPromptMarker(prompt); if (!marker) return false; const cancelledPrompt = this.cancelledGoalPromptMarkers.get(marker); if (!cancelledPrompt || !preservesOwnedPromptAtTerminalBoundary(prompt, cancelledPrompt)) { return false; } this.cancelledGoalPromptMarkers.delete(marker); return true; } consumeCancelledContinuationPrompt(prompt: string) { const marker = extractContinuationMarker(prompt); if (!marker) return false; const cancelledPrompt = this.cancelledContinuationMarkers.get(marker); if (!cancelledPrompt || !preservesOwnedPromptAtTerminalBoundary(prompt, cancelledPrompt)) { return false; } this.cancelledContinuationMarkers.delete(marker); return true; } acceptOwnedInputBoundary(prompt: string) { const fingerprint = inputFingerprint(prompt); const goalMarker = extractGoalPromptMarker(prompt); if (goalMarker) { const pending = this.pendingGoalPromptMarkers.get(goalMarker); if (pending && preservesOwnedPromptAtTerminalBoundary(prompt, pending.prompt)) return true; if (this.claimedGoalPromptMarkers.get(goalMarker) === fingerprint) return true; } const continuationMarker = extractContinuationMarker(prompt); if (!continuationMarker) return false; if ( this.continuationDelivery?.marker === continuationMarker && preservesOwnedPromptAtTerminalBoundary(prompt, this.continuationDelivery.prompt) ) { return true; } return this.claimedContinuationMarkers.get(continuationMarker) === fingerprint; } supersedeOwnedInputCollision(prompt: string) { const fingerprint = inputFingerprint(prompt); const goalMarker = extractGoalPromptMarker(prompt); if (goalMarker) { const pending = this.pendingGoalPromptMarkers.get(goalMarker); if (pending && pending.fingerprint !== fingerprint) { this.pendingGoalPromptMarkers.delete(goalMarker); this.rememberCancelledGoalPromptMarker(goalMarker, pending.prompt); } this.claimedGoalPromptMarkers.delete(goalMarker); } const continuationMarker = extractContinuationMarker(prompt); if (!continuationMarker) return; if ( (this.continuationDelivery?.marker === continuationMarker && !preservesOwnedPromptAtTerminalBoundary(prompt, this.continuationDelivery.prompt)) || this.continuationIntent?.marker === continuationMarker ) { this.cancelContinuationWork(); } this.claimedContinuationMarkers.delete(continuationMarker); } hasOwnedPromptBoundary(prompt: string) { const fingerprint = inputFingerprint(prompt); const goalMarker = extractGoalPromptMarker(prompt); if (goalMarker) { const pending = this.pendingGoalPromptMarkers.get(goalMarker); if ( (pending && preservesOwnedPromptAtTerminalBoundary(prompt, pending.prompt)) || this.claimedGoalPromptMarkers.get(goalMarker) === fingerprint ) { return true; } } const continuationMarker = extractContinuationMarker(prompt); if (!continuationMarker) return false; return ( (this.continuationDelivery?.marker === continuationMarker && preservesOwnedPromptAtTerminalBoundary(prompt, this.continuationDelivery.prompt)) || this.claimedContinuationMarkers.get(continuationMarker) === fingerprint ); } consumeStaleOwnedGoalPrompt(prompt: string) { const marker = extractGoalPromptMarker(prompt); if (!marker) return false; const pending = this.pendingGoalPromptMarkers.get(marker); if (!pending || !preservesOwnedPromptAtTerminalBoundary(prompt, pending.prompt)) return false; if (this.activeGoal?.id === pending.goalId && this.activeGoal.status === "active") { return false; } this.pendingGoalPromptMarkers.delete(marker); return true; } noteQueuedNonGoalInput(prompt: string, behavior: "steer" | "followUp", resetSafetyEpoch = false) { this.pendingNonGoalInputs.push({ behavior, fingerprint: inputFingerprint(prompt), resetSafetyEpoch, }); if (this.pendingNonGoalInputs.length > MAX_PENDING_NON_GOAL_INPUTS) { this.pendingNonGoalInputs.shift(); } } consumeQueuedNonGoalInput(prompt: string, allowDeliveryFallback = true) { if (typeof prompt !== "string") return undefined; const fingerprint = inputFingerprint(prompt); // Pi delivers steers before follow-ups. Prefer a matching steer even when an // identical follow-up was queued first so it cannot steal follow-up ownership. const steerIndex = this.pendingNonGoalInputs.findIndex( (pending) => pending.behavior === "steer" && pending.fingerprint === fingerprint, ); const exactIndex = steerIndex >= 0 ? steerIndex : this.pendingNonGoalInputs.findIndex( (pending) => pending.behavior === "followUp" && pending.fingerprint === fingerprint, ); if (exactIndex >= 0) return this.pendingNonGoalInputs.splice(exactIndex, 1)[0]; if (!allowDeliveryFallback) return undefined; // Skills, templates, and later input handlers can transform the raw text after // pi-goal records it. Fall back to Pi's delivery priority as a bounded marker: // steers drain before follow-ups, and settlement clears stale entries. const fallbackSteerIndex = this.pendingNonGoalInputs.findIndex((pending) => pending.behavior === "steer"); const fallbackIndex = fallbackSteerIndex >= 0 ? fallbackSteerIndex : this.pendingNonGoalInputs.findIndex((pending) => pending.behavior === "followUp"); if (fallbackIndex < 0) return undefined; return this.pendingNonGoalInputs.splice(fallbackIndex, 1)[0]; } consumeQueuedNonGoalFollowUpForAgentStart() { // A pending steer owns the next intra-run boundary. Do not let a later // follow-up suppress cleanup until all earlier-priority steers have started. if (this.pendingNonGoalInputs.some((pending) => pending.behavior === "steer")) return false; const index = this.pendingNonGoalInputs.findIndex((pending) => pending.behavior === "followUp"); if (index < 0) return false; this.pendingNonGoalInputs.splice(index, 1); return true; } markContinuationStarted(prompt: string) { const marker = extractContinuationMarker(prompt); if (!marker) { // A user, retry, or another extension started newer work. Cancel both an // unsent intent and a delivery that may have lost the non-atomic idle race; // the newer work's agent_end will record a fresh intent. this.cancelContinuationWork(); return undefined; } const fingerprint = inputFingerprint(prompt); if (this.continuationDelivery?.marker === marker) { const delivery = this.continuationDelivery; if (preservesOwnedPromptAtTerminalBoundary(prompt, delivery.prompt)) { this.continuationDelivery = undefined; this.rememberClaimedContinuationMarker(marker, prompt); return { goalId: marker.split(":", 1)[0], usageLimitRetry: delivery.usageLimitRetry }; } } const cancelledPrompt = this.cancelledContinuationMarkers.get(marker); if ( this.claimedContinuationMarkers.get(marker) === fingerprint || (cancelledPrompt && preservesOwnedPromptAtTerminalBoundary(prompt, cancelledPrompt)) ) { return { goalId: marker.split(":", 1)[0], usageLimitRetry: undefined }; } this.cancelContinuationWork(); return undefined; } persistGoal(goal: ActiveGoal) { if (!isTerminalGoalStatus(goal.status) || this.terminalDetails?.goalId !== goal.id) { this.clearTerminalDetails(); } this.pi.appendEntry(GOAL_STATE_ENTRY_TYPE, serializeGoalState(goal)); this.publishGoalState(buildGoalStateSnapshot(goal, this.terminalDetails?.summary, this.terminalDetails?.reason)); } clearPersistedGoal(cwd: string, clearedGoal?: ActiveGoal, reason = "goal cleared") { this.pi.appendEntry(GOAL_STATE_ENTRY_TYPE, serializeGoalState(undefined)); if (clearedGoal) { this.publishGoalState({ goalId: clearedGoal.id, status: "cleared", reason, }); } this.clearTerminalDetails(); clearLegacyPersistedGoal(cwd); } clearActiveGoal(ctx: StatusContext, reason = "goal cleared", releaseWorkflow = true) { this.clearActiveGoalState(ctx, reason, releaseWorkflow); this.ensureInactiveGoalContextContract(ctx); } clearCompletedGoal(ctx: StatusContext) { this.clearActiveGoalState(ctx, "goal cleared", true); } private clearActiveGoalState(ctx: StatusContext, reason: string, releaseWorkflow: boolean) { const clearedGoal = this.activeGoal; this.clearGoalWaitTimer(); this.cancelContinuationWork(); this.clearGoalRecovery(); this.clearBudgetWrapUp(); this.clearStaleGoalToolCallBlock(); this.activeGoal = undefined; this.legacyQueueState = undefined; this.clearPersistedGoal(ctx.cwd, clearedGoal, reason); ctx.ui.setStatus(STATUS_KEY, undefined); if (releaseWorkflow) this.releaseWorkflow(); } snapshotSettingsApplicationState(): GoalSettingsRuntimeSnapshot { return { settings: structuredClone(this.settings), activeGoal: this.activeGoal ? structuredClone(this.activeGoal) : undefined, legacyQueueState: this.legacyQueueState ? structuredClone(this.legacyQueueState) : undefined, legacyExperimentalGoalsSetting: this.legacyExperimentalGoalsSetting, continuationIntent: this.continuationIntent ? structuredClone(this.continuationIntent) : undefined, continuationDelivery: this.continuationDelivery ? structuredClone(this.continuationDelivery) : undefined, goalRecovery: this.goalRecovery ? structuredClone(this.goalRecovery) : undefined, pendingLengthRecovery: this.pendingLengthRecovery ? structuredClone(this.pendingLengthRecovery) : undefined, budgetWrapUp: this.budgetWrapUp ? structuredClone(this.budgetWrapUp) : undefined, guardAbortGoalId: this.guardAbortGoalId, staleGoalToolCallsBlocked: this.staleGoalToolCallsBlocked, cancelledContinuationMarkers: [...this.cancelledContinuationMarkers], terminalDetails: this.terminalDetails ? structuredClone(this.terminalDetails) : undefined, }; } restoreSettingsApplicationState(snapshot: GoalSettingsRuntimeSnapshot) { if (snapshot.activeGoal?.status === "active" && !this.acquireWorkflow()) { throw new Error("another workflow became active before Goal settings could roll back"); } this.settings = structuredClone(snapshot.settings); this.activeGoal = snapshot.activeGoal ? structuredClone(snapshot.activeGoal) : undefined; this.legacyQueueState = snapshot.legacyQueueState ? structuredClone(snapshot.legacyQueueState) : undefined; this.legacyExperimentalGoalsSetting = snapshot.legacyExperimentalGoalsSetting; this.continuationIntent = snapshot.continuationIntent ? structuredClone(snapshot.continuationIntent) : undefined; this.continuationDelivery = snapshot.continuationDelivery ? structuredClone(snapshot.continuationDelivery) : undefined; this.goalRecovery = snapshot.goalRecovery ? structuredClone(snapshot.goalRecovery) : undefined; this.pendingLengthRecovery = snapshot.pendingLengthRecovery ? structuredClone(snapshot.pendingLengthRecovery) : undefined; this.budgetWrapUp = snapshot.budgetWrapUp ? structuredClone(snapshot.budgetWrapUp) : undefined; this.guardAbortGoalId = snapshot.guardAbortGoalId; this.staleGoalToolCallsBlocked = snapshot.staleGoalToolCallsBlocked; this.cancelledContinuationMarkers = new Map(snapshot.cancelledContinuationMarkers); this.terminalDetails = snapshot.terminalDetails ? structuredClone(snapshot.terminalDetails) : undefined; } pauseGoalForUnavailableTools(ctx: StatusContext, abortTurn = true, recordUsage = true) { const goal = this.activeGoal; if (goal?.status !== "active") return false; const stoppedGoal = this.stopActiveGoal(ctx, { kind: "tools_unavailable", expectedGoalId: goal.id, abortTurn, recordUsage, }); if (!stoppedGoal) return false; notifyTerminal( ctx.ui, "Goal tools are unavailable, so the active goal was paused. Restore the tools and run /goal resume.", "warning", ); return true; } showCompletionStatus(ctx: StatusContext) { this.clearCompletionStatusTimer(); ctx.ui.setStatus(STATUS_KEY, "complete"); this.completionStatusTimer = setTimeout(() => { this.completionStatusTimer = undefined; try { ctx.ui.setStatus(STATUS_KEY, undefined); } catch { // The completion status is best-effort; the captured ctx may be stale after // session replacement or reload before this timer fires. } }, 8_000); this.completionStatusTimer.unref?.(); } clearCompletionStatusTimer() { if (!this.completionStatusTimer) return; clearTimeout(this.completionStatusTimer); this.completionStatusTimer = undefined; } private rememberPendingGoalPrompt(goalId: string, prompt: string, resetSafetyEpoch: boolean) { const marker = randomUUID(); const ownedPrompt = appendGoalPromptMarker(prompt, marker); this.pendingGoalPromptMarkers.set(marker, { goalId, resetSafetyEpoch, fingerprint: inputFingerprint(ownedPrompt), prompt: ownedPrompt, }); if (this.pendingGoalPromptMarkers.size > MAX_PENDING_GOAL_PROMPTS) { const oldest = this.pendingGoalPromptMarkers.keys().next().value; if (oldest) this.pendingGoalPromptMarkers.delete(oldest); } return { marker, prompt: ownedPrompt }; } private consumePendingGoalPrompt(prompt: string) { const marker = extractGoalPromptMarker(prompt); if (!marker) return undefined; const pending = this.pendingGoalPromptMarkers.get(marker); if (!pending || !preservesOwnedPromptAtTerminalBoundary(prompt, pending.prompt)) { return undefined; } this.pendingGoalPromptMarkers.delete(marker); this.rememberClaimedGoalPromptMarker(marker, inputFingerprint(prompt)); return pending; } private rememberCancelledGoalPromptMarker(marker: string, prompt: string) { this.cancelledGoalPromptMarkers.set(marker, prompt); if (this.cancelledGoalPromptMarkers.size <= MAX_PENDING_GOAL_PROMPTS) return; const oldest = this.cancelledGoalPromptMarkers.keys().next().value; if (oldest) this.cancelledGoalPromptMarkers.delete(oldest); } private rememberClaimedGoalPromptMarker(marker: string, fingerprint: string) { this.claimedGoalPromptMarkers.set(marker, fingerprint); if (this.claimedGoalPromptMarkers.size <= MAX_PENDING_GOAL_PROMPTS) return; const oldest = this.claimedGoalPromptMarkers.keys().next().value; if (oldest) this.claimedGoalPromptMarkers.delete(oldest); } private rememberClaimedContinuationMarker(marker: string, prompt: string) { this.claimedContinuationMarkers.set(marker, inputFingerprint(prompt)); if (this.claimedContinuationMarkers.size <= MAX_CANCELLED_CONTINUATION_PROMPTS) return; const oldest = this.claimedContinuationMarkers.keys().next().value; if (oldest) this.claimedContinuationMarkers.delete(oldest); } consumeOwnedGoalPrompt(prompt: string) { return this.consumePendingGoalPrompt(prompt); } private rememberCancelledContinuationMarker(ticket: ContinuationTicket) { this.cancelledContinuationMarkers.set(ticket.marker, ticket.prompt); if (this.cancelledContinuationMarkers.size <= MAX_CANCELLED_CONTINUATION_PROMPTS) return; const oldest = this.cancelledContinuationMarkers.keys().next().value; if (oldest) this.cancelledContinuationMarkers.delete(oldest); } } export function createGoal(text: string, tokenBudget: number | undefined, baselineTokens: number): ActiveGoal { const now = Date.now(); return { id: randomUUID(), text, status: "active", startedAt: now, updatedAt: now, iteration: 0, tokenBudget, tokensUsed: 0, timeUsedSeconds: 0, baselineTokens, activeStartedAt: now, automaticModelTurns: 0, toolFreeRepeatCount: 0, }; } export function transitionGoal(goal: ActiveGoal, requestedStatus: GoalStatus): ActiveGoal { const now = Date.now(); const status = requestedStatus === "active" && goal.tokenBudget !== undefined && goal.tokensUsed >= goal.tokenBudget ? "budget_limited" : requestedStatus; const next = { ...goal, status, updatedAt: now, ...(status === "active" ? {} : { waiting: undefined }), }; checkpointGoalActiveTime(next, now, status === "active" && !next.waiting); return next; } export function nextGoalInstance(goal: ActiveGoal): ActiveGoal { return { ...goal, id: randomUUID(), updatedAt: Date.now() }; } export function editedGoalStatus(status: GoalStatus): GoalStatus { if (status === "paused" || status === "blocked" || status === "usage_limited") return status; return "active"; } export function incrementGoal(goal: ActiveGoal): ActiveGoal { return { ...goal, iteration: goal.iteration + 1, updatedAt: Date.now() }; } export function formatStatus( goal: ActiveGoal | undefined, automaticTurnLimit: number | null = DEFAULT_GOAL_SETTINGS.continuationLimits.automaticTurns, ) { if (!goal) return undefined; if (goal.status === "complete") return "complete"; const automatic = automaticTurnLimit === null ? "automatic Unlimited" : `automatic ${goal.automaticModelTurns}/${automaticTurnLimit}`; if (goal.waiting) { return `waiting ${safeGoalMenuText(goal.waiting.reason)} · ${automatic}`; } if (goal.status === "paused" && goal.safetyPauseCause === "continuation_limit") { if (automaticTurnLimit === null) { return `paused · previous automatic limit at ${goal.automaticModelTurns}`; } if (goal.automaticModelTurns < automaticTurnLimit) { return `paused · automatic ${goal.automaticModelTurns}/${automaticTurnLimit}`; } return `paused · automatic limit ${goal.automaticModelTurns}/${automaticTurnLimit}`; } if (goal.status === "paused") return `paused · ${automatic}`; if (goal.status === "blocked") return `blocked · ${automatic}`; if (goal.status === "usage_limited") return `usage · ${automatic}`; if (goal.status === "budget_limited") return `budget ${formatBudget(goal)} · ${automatic}`; if (goal.tokenBudget !== undefined) return `active ${formatBudget(goal)} · ${automatic}`; return `active ${formatDuration(goal.timeUsedSeconds)} · ${automatic}`; } export function formatBudget(goal: ActiveGoal) { return `${formatTokenCount(goal.tokensUsed)}/${formatTokenCount(goal.tokenBudget ?? 0)}`; } export function goalSummary( goal: ActiveGoal, automaticTurnLimit: number | null = DEFAULT_GOAL_SETTINGS.continuationLimits.automaticTurns, ) { const summary = [ `Goal: ${goal.text}`, `Status: ${goal.waiting ? "waiting" : goal.status}`, ...(goal.waiting ? [ `Waiting: ${safeGoalMenuText(goal.waiting.reason, 1_000)}`, ...(goal.waiting.resumeAt === undefined ? [] : [`Resume deadline: ${new Date(goal.waiting.resumeAt).toISOString()}`]), ] : []), `Iteration: ${goal.iteration}`, automaticTurnLimit === null ? `Automatic work: ${goal.automaticModelTurns} responses · Unlimited` : `Automatic work: ${goal.automaticModelTurns} of ${automaticTurnLimit} responses`, `Active elapsed: ${formatDuration(goal.timeUsedSeconds)}`, `Tokens: ${goal.tokenBudget === undefined ? formatTokenCount(goal.tokensUsed) : formatBudget(goal)}`, ]; if (goal.safetyPauseCause) { summary.push( goal.safetyPauseCause === "continuation_limit" ? `Safety pause: automatic-work limit reached (${goal.automaticModelTurns} of ${automaticTurnLimit ?? "Unlimited"} responses). Progress is saved; open /goal to review and continue.` : "Safety pause: no progress. Progress is saved; open /goal to review and continue.", ); } summary.push(`Commands: ${goalCommandHint(goal)}`); return summary.join("\n"); } export function hasPendingMessages(ctx: StatusContext) { return ctx.hasPendingMessages?.() ?? false; } export function abortCurrentTurn(ctx: StatusContext) { try { ctx.abort?.(); } catch { // Best effort: stale goal guards still prevent follow-on tool calls. } } export function blocksStaleGoalToolCalls(status: GoalStatus) { return status === "paused" || status === "blocked" || status === "usage_limited" || status === "budget_limited"; } export function isResumableGoalStatus(status: GoalStatus) { return blocksStaleGoalToolCalls(status); } export function stoppedStatusLabel(status: GoalStatus) { if (status === "usage_limited") return "usage-limited"; if (status === "budget_limited") return "budget-limited"; return status; } export function isContradictoryCompletionSummary(summary: string) { return CONTRADICTORY_COMPLETION_PATTERNS.some((pattern) => pattern.test(summary)); } export function goalIdRejectionReason(goal: ActiveGoal, requestedGoalId: string) { if (!requestedGoalId) return "missing goal_id"; if (requestedGoalId.length > MAX_GOAL_ID_LENGTH) return "goal_id is too long"; if (requestedGoalId !== goal.id) return "goal_id does not match the active goal"; return undefined; } function goalWaitSessionId(session: unknown): unknown { const manager = session as { getSessionId?: () => unknown } | undefined; return typeof manager?.getSessionId === "function" ? manager.getSessionId() : undefined; } function preservesOwnedPromptAtTerminalBoundary(prompt: string, ownedPrompt: string) { // Earlier input handlers may prefix a live pi-goal message before this runtime // can fingerprint it. Require the complete generated prompt as the terminal // boundary so a quoted marker or text appended by an external sender is not owned. return prompt === ownedPrompt || prompt.endsWith(ownedPrompt); } function inputFingerprint(prompt: unknown) { return createHash("sha256") .update(typeof prompt === "string" ? prompt : "", "utf8") .digest("hex"); } function goalContractEntries(ctx: StatusContext) { const sessionManager = ctx.sessionManager as | { buildContextEntries?: () => unknown[]; buildSessionContext?: () => { messages?: unknown[] }; getBranch?: () => unknown[]; getEntries?: () => unknown[]; } | undefined; const historyEntries = sessionManager?.getBranch?.() ?? sessionManager?.getEntries?.() ?? []; return { contextEntries: sessionManager?.buildSessionContext?.().messages ?? sessionManager?.buildContextEntries?.() ?? historyEntries, historyEntries, }; } async function sendPrompt(pi: ExtensionAPI, ctx: StatusContext, prompt: string, isCurrent?: () => boolean) { try { await pi.sendUserMessage(prompt, { deliverAs: "followUp" }); return true; } catch (error) { if (!isCurrent || isCurrent()) { notifyWhenSessionAlive(ctx, `Goal prompt failed: ${formatError(error)}`, "error"); } return false; } } // A delayed callback can outlive its session: after replacement or reload Pi // throws a stale-context error from every ctx getter, so reporting through the // dead session's UI must not turn into another throw inside a timer or a catch // block. function notifyWhenSessionAlive(ctx: StatusContext, message: string, level?: "info" | "warning" | "error") { try { notifyTerminal(ctx.ui, message, level); } catch (error) { if (!isStaleContextError(error)) throw error; } } function goalCommandHint(goal: ActiveGoal) { if (goal.waiting) { return "/goal resume, /goal edit , /goal pause, /goal clear"; } if (goal.status === "active") { return "/goal edit , /goal pause, /goal clear"; } if (isResumableGoalStatus(goal.status)) { return "/goal edit , /goal resume, /goal clear"; } return "/goal edit , /goal clear"; } function continuationMarker(goal: ActiveGoal) { return `${goal.id}:${goal.iteration}:${randomUUID()}`; } export type { AssistantMessageLike } from "./errors.js"; export { findFinalAssistantMessage, formatError, isGoalContextOverflow, isRetryableGoalInterruption, isUsageLimitedGoalInterruption, truncateNotification, } from "./errors.js";