import { createHash } from "node:crypto"; import type { ExtensionContext } from "@selesai/code"; import { computeWatchdogRepoChangeSignature, eventIndicatesRepoEdit, type WatchdogRepoChangeSignature } from "./change-signature.ts"; import { WatchdogEmissionGuard } from "./emission-guard.ts"; import { collectWatchdogLspDiagnostics, formatWatchdogLspDiagnosticsBlock, WatchdogLspDiagnosticsLedger, watchdogWarningFromLspDiagnostics, type WatchdogLspDiagnosticsFunction, } from "./lsp-diagnostics.ts"; import { WatchdogScopeArtifact, isWatchdogAutoFollowPromptEvent } from "./scope.ts"; import { resolveWatchdogConfig } from "./settings.ts"; import { formatWatchdogTurnDelta } from "./turn-delta.ts"; import { type ResolvedWatchdogConfig, type WatchdogEndpointConfig, type WatchdogLspRuntimeSnapshot, type WatchdogRuntimeStatus, type WatchdogSettingsError, type WatchdogSettingsResult, type WatchdogSettingsSource, type WatchdogWarning, type WatchdogWarningDetails, } from "./types.ts"; import { normalizeWatchdogWarningDetails } from "./warning-format.ts"; type ReviewStopReason = "stop" | "error" | "aborted" | "length"; export interface WatchdogReviewResult { warnings?: WatchdogWarning[]; stopReason?: ReviewStopReason; } export interface WatchdogReviewRequest { delta: string; epoch: number; hasScope: boolean; reviewId: number; config: ResolvedWatchdogConfig; emitWarning(warning: WatchdogWarning): boolean; signal?: AbortSignal; } export type WatchdogReviewFunction = (request: WatchdogReviewRequest) => Promise | WatchdogReviewResult | void; export interface WatchdogRuntimeSnapshot { status: WatchdogRuntimeStatus; enabled: boolean; config: ResolvedWatchdogConfig; configOk: boolean; errors: WatchdogSettingsError[]; sources: WatchdogSettingsSource[]; bufferedDeltas: number; epoch: number; activeReviewId?: number; sessionOverride?: boolean; sessionModelOverride?: Partial>; lastWarning?: WatchdogWarningDetails; lastError?: string; failedReviews: number; staleReviews: number; reviewConnected: boolean; reviewDescription: string; autoFollowQueued: boolean; autoFollowAttempts: number; autoFollowStalemate: boolean; reviewTrigger: "turn-delta" | "repo-edits"; changedPaths?: string[]; lsp: WatchdogLspRuntimeSnapshot; } interface Waiter { resolve(settled: boolean): void; timer: ReturnType; } interface MainWatchdogRuntimeOptions { cwd?: string; resolveConfig?: (cwd: string, options?: { session?: Record }) => WatchdogSettingsResult; review?: WatchdogReviewFunction; reviewDescription?: string; displayWarning?: (warning: WatchdogWarningDetails, options?: { deliverAs?: "steer" }) => void; sendUserMessage?: (message: string) => void | Promise; reviewChangesOnly?: boolean; lspDiagnostics?: WatchdogLspDiagnosticsFunction; repoChangeSignature?: typeof computeWatchdogRepoChangeSignature; } type ContextLike = Pick; type ReviewDeltaOutcome = "completed" | "timeout" | "stale"; const DEFAULT_REVIEW: WatchdogReviewFunction = () => ({ warnings: [] }); const MAX_REVIEW_INPUT_CHARS = 24_000; const REVIEW_DELTA_SEPARATOR = "\n\n---\n\n"; function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function promptFromBeforeAgentStart(event: unknown): string | undefined { if (!event || typeof event !== "object") return undefined; const input = event as { prompt?: unknown; systemPrompt?: unknown }; if (typeof input.prompt === "string") return input.prompt; if (typeof input.systemPrompt === "string") return input.systemPrompt; return undefined; } function reviewInputSignature(input: string): string { return createHash("sha256").update(input).digest("hex"); } export class MainWatchdogRuntime { private cwd: string; private readonly resolveConfig: (cwd: string, options?: { session?: Record }) => WatchdogSettingsResult; private readonly review: WatchdogReviewFunction; private readonly reviewConnected: boolean; private readonly reviewDescription: string; private readonly displayWarning: ((warning: WatchdogWarningDetails, options?: { deliverAs?: "steer" }) => void) | undefined; private readonly sendUserMessage: ((message: string) => void | Promise) | undefined; private readonly reviewChangesOnly: boolean; private readonly lspDiagnostics: WatchdogLspDiagnosticsFunction; private readonly repoChangeSignature: typeof computeWatchdogRepoChangeSignature; private readonly lspLedger = new WatchdogLspDiagnosticsLedger(); private readonly scope = new WatchdogScopeArtifact(); private configResult: WatchdogSettingsResult; private sessionOverrideEnabled: boolean | undefined; private sessionModelOverride: Partial> | undefined; private status: WatchdogRuntimeStatus = "idle"; private pendingDeltas: string[] = []; private pendingDeltaChars = 0; private guard = new WatchdogEmissionGuard(); private guardMaxWarnings: number | null = null; private epoch = 0; private reviewIdCounter = 0; private agentEndIdCounter = 0; private activeAgentEndId: number | undefined; private activeAgentEndAbortController: AbortController | undefined; private activeReviewId: number | undefined; private activeReviewWarning: WatchdogWarningDetails | undefined; private reviewing = false; private waitingAtAgentEnd = false; private disposed = false; private includeUserPromptInNextDelta = false; private userPrompt: string | undefined; private waiters: Waiter[] = []; private lastWarning: WatchdogWarningDetails | undefined; private displayedWarningSequence = 0; private lastError: string | undefined; private lastReviewInputSignature: string | undefined; private turnStartChangeSignature: WatchdogRepoChangeSignature | undefined; private lastReviewedChangeSignature: string | undefined; private currentChangedPaths: string[] | undefined; private lastLspSnapshot: WatchdogLspRuntimeSnapshot | undefined; private observedRepoEditThisTurn = false; private toolResultsThisRun = 0; private midRunReviewing = false; private autoFollowQueued = false; private autoFollowAttempts = 0; private consecutiveAutoFollowIdentity: string | undefined; private consecutiveAutoFollowRepeats = 0; private autoFollowStalemate = false; private pendingAutoFollowPrompts: string[] = []; private midRunGeneration = 0; private activeReviewAbortController: AbortController | undefined; private failedReviews = 0; private staleReviews = 0; constructor(options: MainWatchdogRuntimeOptions = {}) { this.cwd = options.cwd ?? process.cwd(); this.resolveConfig = options.resolveConfig ?? resolveWatchdogConfig; this.review = options.review ?? DEFAULT_REVIEW; this.reviewConnected = Boolean(options.review); this.reviewDescription = options.reviewDescription ?? (options.review ? "injected seam" : "not wired"); this.displayWarning = options.displayWarning; this.sendUserMessage = options.sendUserMessage; this.reviewChangesOnly = options.reviewChangesOnly === true; this.lspDiagnostics = options.lspDiagnostics ?? collectWatchdogLspDiagnostics; this.repoChangeSignature = options.repoChangeSignature ?? computeWatchdogRepoChangeSignature; this.configResult = this.resolveConfig(this.cwd); this.guardMaxWarnings = this.configResult.config.maxWarnings; this.guard = new WatchdogEmissionGuard({ maxWarnings: this.guardMaxWarnings }); this.turnStartChangeSignature = this.currentRepoChangeSignature(); this.lastReviewedChangeSignature = this.turnStartChangeSignature?.key; } bindSession(ctx: ContextLike): void { this.cwd = ctx.cwd; this.sessionOverrideEnabled = undefined; this.sessionModelOverride = undefined; this.refreshConfig(ctx.cwd); this.reset("session_start", { clearReviewInputSignature: true, resetChangeSignature: true, clearLspLedger: true, clearScope: true, resetAutoFollow: true }); } refreshConfig(cwd = this.cwd): WatchdogSettingsResult { this.cwd = cwd; const wasEnabled = this.isEnabled(); const session = this.sessionOverrideEnabled === undefined && this.sessionModelOverride === undefined ? undefined : { ...(this.sessionOverrideEnabled === undefined ? {} : { enabled: this.sessionOverrideEnabled }), main: { ...(this.sessionOverrideEnabled === undefined ? {} : { enabled: this.sessionOverrideEnabled }), ...(this.sessionModelOverride ?? {}), }, }; this.configResult = this.resolveConfig(this.cwd, session === undefined ? undefined : { session }); if (this.configResult.config.maxWarnings !== this.guardMaxWarnings) { this.guardMaxWarnings = this.configResult.config.maxWarnings; this.guard = new WatchdogEmissionGuard({ maxWarnings: this.guardMaxWarnings }); } if (wasEnabled && !this.isEnabled()) this.invalidateActiveReview("watchdog disabled"); return this.configResult; } setSessionEnabled(enabled: boolean, cwd = this.cwd): WatchdogRuntimeSnapshot { this.sessionOverrideEnabled = enabled; this.reset("session override"); this.refreshConfig(cwd); return this.getSnapshot(); } setSessionModel(patch: { model?: string | null; thinking?: WatchdogEndpointConfig["thinking"] | null }, cwd = this.cwd): WatchdogRuntimeSnapshot { const next = { ...(this.sessionModelOverride ?? {}) }; if (patch.model === null) delete next.model; else if (patch.model !== undefined) next.model = patch.model; if (patch.thinking === null) delete next.thinking; else if (patch.thinking !== undefined) next.thinking = patch.thinking; this.sessionModelOverride = next.model === undefined && next.thinking === undefined ? undefined : next; this.reset("session model override"); this.refreshConfig(cwd); return this.getSnapshot(); } clearSessionModel(cwd = this.cwd): WatchdogRuntimeSnapshot { this.sessionModelOverride = undefined; this.reset("session model override cleared"); this.refreshConfig(cwd); return this.getSnapshot(); } clearSessionOverride(cwd = this.cwd): WatchdogRuntimeSnapshot { this.sessionOverrideEnabled = undefined; this.sessionModelOverride = undefined; this.reset("session override cleared"); this.refreshConfig(cwd); return this.getSnapshot(); } reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean; resetAutoFollow?: boolean } = {}): void { this.abortActiveAgentEnd(); this.epoch++; this.status = "idle"; this.clearPendingDeltas(); this.reviewing = false; this.waitingAtAgentEnd = false; this.activeReviewId = undefined; this.activeReviewWarning = undefined; this.includeUserPromptInNextDelta = false; this.userPrompt = undefined; this.lastError = undefined; this.currentChangedPaths = undefined; this.observedRepoEditThisTurn = false; this.toolResultsThisRun = 0; this.midRunReviewing = false; this.autoFollowQueued = false; if (options.clearLspLedger) { this.lspLedger.reset(); this.lastLspSnapshot = undefined; } if (options.clearScope) { this.scope.reset(); this.pendingAutoFollowPrompts = []; } if (options.resetAutoFollow) this.resetAutoFollowState(); if (options.clearReviewInputSignature) this.lastReviewInputSignature = undefined; if (options.resetChangeSignature) this.resetRepoChangeBaseline({ reviewed: true }); this.guard.reset(); this.resolveWaiters(true); } dispose(): void { this.disposed = true; this.abortActiveAgentEnd(); this.epoch++; this.status = "idle"; this.clearPendingDeltas(); this.reviewing = false; this.waitingAtAgentEnd = false; this.activeReviewId = undefined; this.activeReviewWarning = undefined; this.lastReviewInputSignature = undefined; this.currentChangedPaths = undefined; this.lastLspSnapshot = undefined; this.lspLedger.reset(); this.scope.reset(); this.observedRepoEditThisTurn = false; this.toolResultsThisRun = 0; this.midRunReviewing = false; this.autoFollowQueued = false; this.resolveWaiters(false); } handleBeforeAgentStart(event: unknown, ctx: ContextLike): void { if (this.disposed) return; const incomingPrompt = promptFromBeforeAgentStart(event); // Only exact queued auto-follow text is treated as an auto-follow turn; a real user // prompt racing in ahead of one stays real and the pending matches survive for later. const pendingIndex = incomingPrompt === undefined ? -1 : this.pendingAutoFollowPrompts.indexOf(incomingPrompt); const autoFollowPrompt = pendingIndex >= 0 || isWatchdogAutoFollowPromptEvent(event); if (pendingIndex >= 0) this.pendingAutoFollowPrompts.splice(pendingIndex, 1); this.reset("before_agent_start", { resetAutoFollow: !autoFollowPrompt }); this.refreshConfig(ctx.cwd); this.userPrompt = incomingPrompt; if (!autoFollowPrompt && this.userPrompt?.trim()) { this.includeUserPromptInNextDelta = true; this.scope.addPrompt(this.userPrompt); } else { this.includeUserPromptInNextDelta = false; } this.resetRepoChangeBaseline(); } handleTurnEnd(event: unknown, ctx: ContextLike): void { if (this.disposed) return; this.refreshConfig(ctx.cwd); if (!this.isEnabled()) return; try { this.observedRepoEditThisTurn ||= eventIndicatesRepoEdit(event); const delta = formatWatchdogTurnDelta({ includeUserPrompt: this.includeUserPromptInNextDelta, userPrompt: this.userPrompt, events: [event], }); this.includeUserPromptInNextDelta = false; this.enqueueDelta(delta); } catch (error) { this.fail(`Failed to format watchdog turn delta: ${errorMessage(error)}`); } } enqueueDelta(delta: string): void { if (this.disposed || !delta.trim() || !this.isEnabled()) return; this.appendBoundedDelta(delta); if (!this.reviewing && !this.waitingAtAgentEnd && !this.midRunReviewing) this.status = "queued"; } handleToolResult(ctx: ContextLike): void { if (this.disposed) return; this.refreshConfig(ctx.cwd); if (!this.isEnabled()) return; const everyNTools = this.configResult.config.cadence.everyNTools; if (everyNTools === null) return; this.toolResultsThisRun++; if (this.toolResultsThisRun % everyNTools !== 0) return; if (this.reviewing || this.waitingAtAgentEnd || this.midRunReviewing) return; const delta = this.buildReviewInput(undefined, ""); if (!delta.trim()) return; void this.reviewMidRunDelta(delta); } async handleAgentEnd(_event: unknown, ctx: ContextLike): Promise { if (this.disposed) return; this.refreshConfig(ctx.cwd); if (!this.isEnabled()) return; const changeSignature = this.resolveReviewChangeSignature(ctx.cwd); if (this.reviewChangesOnly && !changeSignature) { this.clearPendingDeltas(); if (this.status === "queued") this.status = "idle"; this.resolveWaiters(true); return; } if (changeSignature && changeSignature.key === this.lastReviewedChangeSignature) { this.clearPendingDeltas(); this.status = "idle"; this.resolveWaiters(true); return; } this.cancelMidRunReview(); this.waitingAtAgentEnd = true; const agentEndEpoch = this.epoch; const agentEndId = ++this.agentEndIdCounter; const lspAbortController = new AbortController(); this.activeAgentEndId = agentEndId; this.activeAgentEndAbortController = lspAbortController; let displayedDuringAgentEnd: WatchdogWarningDetails | undefined; const previousDisplayedSequence = this.displayedWarningSequence; try { this.guard.startModelUpdate(); const lspBlock = await this.collectLspDiagnostics(changeSignature, { epoch: agentEndEpoch, agentEndId, signal: lspAbortController.signal, }); if (this.activeAgentEndAbortController === lspAbortController) this.activeAgentEndAbortController = undefined; if (!this.isAgentEndCurrent(agentEndEpoch, agentEndId)) return; const delta = this.buildReviewInput(changeSignature, lspBlock); this.clearPendingDeltas(); if (!delta.trim()) { this.waitingAtAgentEnd = false; if (this.status === "queued") this.status = "idle"; this.resolveWaiters(true); return; } const signature = reviewInputSignature(delta); if (!this.reviewChangesOnly && signature === this.lastReviewInputSignature) { this.waitingAtAgentEnd = false; this.status = "idle"; this.resolveWaiters(true); return; } const outcome = await this.reviewDelta(delta, this.configResult.config.agentEndTimeoutMs); this.waitingAtAgentEnd = false; if (outcome === "timeout") { this.staleReviews++; this.invalidateActiveReview("agent-end timeout"); this.status = "stale"; this.markLastWarningStale(); this.resolveWaiters(true); return; } if (outcome === "completed" && this.status !== "failed" && this.status !== "stale") { this.lastReviewInputSignature = signature; if (changeSignature) this.lastReviewedChangeSignature = changeSignature.key; this.currentChangedPaths = changeSignature?.changedPaths; this.status = "idle"; } displayedDuringAgentEnd = this.displayedWarningSequence !== previousDisplayedSequence ? this.lastWarning : undefined; this.queueAutoFollowIfNeeded(displayedDuringAgentEnd); this.resolveWaiters(true); } finally { if (this.activeAgentEndAbortController === lspAbortController) this.activeAgentEndAbortController = undefined; if (this.activeAgentEndId === agentEndId) this.activeAgentEndId = undefined; } } recordDisplayedWarning(warning: WatchdogWarning): WatchdogWarningDetails { const details = normalizeWatchdogWarningDetails(warning, { state: "displayed", source: warning.source ?? "main" }); this.lastWarning = details; return details; } getSnapshot(cwd?: string): WatchdogRuntimeSnapshot { if (cwd) this.refreshConfig(cwd); return { status: this.status, enabled: this.isEnabled(), config: this.configResult.config, configOk: this.configResult.ok, errors: [...this.configResult.errors], sources: [...this.configResult.sources], bufferedDeltas: this.pendingDeltas.length, epoch: this.epoch, ...(this.activeReviewId !== undefined ? { activeReviewId: this.activeReviewId } : {}), ...(this.sessionOverrideEnabled !== undefined ? { sessionOverride: this.sessionOverrideEnabled } : {}), ...(this.sessionModelOverride !== undefined ? { sessionModelOverride: { ...this.sessionModelOverride } } : {}), ...(this.lastWarning ? { lastWarning: this.lastWarning } : {}), ...(this.lastError ? { lastError: this.lastError } : {}), failedReviews: this.failedReviews, staleReviews: this.staleReviews, reviewConnected: this.reviewConnected, reviewDescription: this.reviewDescription, autoFollowQueued: this.autoFollowQueued, autoFollowAttempts: this.autoFollowAttempts, autoFollowStalemate: this.autoFollowStalemate, reviewTrigger: this.reviewChangesOnly ? "repo-edits" : "turn-delta", ...(this.currentChangedPaths?.length ? { changedPaths: [...this.currentChangedPaths] } : {}), lsp: this.lspSnapshot(), }; } async waitForIdle(timeoutMs = 1_000): Promise { return this.waitForSettled(timeoutMs); } private isEnabled(): boolean { return this.configResult.ok && this.configResult.config.main.enabled; } private abortActiveAgentEnd(): void { this.activeAgentEndAbortController?.abort(); this.activeAgentEndAbortController = undefined; this.activeAgentEndId = undefined; } private isAgentEndCurrent(epoch: number, agentEndId: number): boolean { return !this.disposed && this.epoch === epoch && this.activeAgentEndId === agentEndId && this.waitingAtAgentEnd && this.isEnabled(); } private isCurrent(epoch: number, reviewId: number): boolean { return !this.disposed && this.epoch === epoch && this.activeReviewId === reviewId; } private warningMeetsThreshold(warning: WatchdogWarning): boolean { return this.configResult.config.severityThreshold === "concern" || warning.severity === "blocker"; } private acceptWarning(epoch: number, reviewId: number, warning: WatchdogWarning): boolean { if (!this.isCurrent(epoch, reviewId) || !this.isEnabled() || !this.warningMeetsThreshold(warning)) return false; const decision = this.guard.evaluate(warning); if (!decision.accepted) return false; const details = normalizeWatchdogWarningDetails(warning, { state: "candidate", source: warning.source ?? "main", identity: decision.identity, }); this.lastWarning = details; this.activeReviewWarning = details; return true; } private displayBoundaryWarning(warning: WatchdogWarning): boolean { if (!this.isEnabled() || !this.warningMeetsThreshold(warning)) return false; const decision = this.guard.evaluate(warning); if (!decision.accepted) return false; const details = normalizeWatchdogWarningDetails(warning, { state: "displayed", source: warning.source ?? "main", identity: decision.identity, displayedAt: new Date().toISOString(), }); this.lastWarning = details; this.displayedWarningSequence++; this.displayWarning?.(details); return true; } private invalidateActiveReview(_reason: string): void { this.abortActiveAgentEnd(); this.epoch++; this.status = "idle"; this.clearPendingDeltas(); this.reviewing = false; this.waitingAtAgentEnd = false; this.activeReviewId = undefined; this.activeReviewWarning = undefined; } private async reviewMidRunDelta(delta: string): Promise { if (this.midRunReviewing || this.reviewing || this.waitingAtAgentEnd || this.disposed) return; this.midRunReviewing = true; const generation = this.midRunGeneration; try { const outcome = await this.reviewDelta(delta, this.configResult.config.agentEndTimeoutMs, { correction: true }); if (generation !== this.midRunGeneration) return; if (outcome === "timeout") { this.staleReviews++; this.status = "stale"; this.markLastWarningStale(); } } finally { if (generation === this.midRunGeneration) { this.midRunReviewing = false; if (this.status === "reviewing") this.status = this.pendingDeltas.length ? "queued" : "idle"; this.resolveWaiters(this.isSettled()); } } } // The agent-end boundary review is authoritative; an in-flight cadence review is superseded. private cancelMidRunReview(): void { if (!this.midRunReviewing) return; this.midRunGeneration++; this.activeReviewAbortController?.abort(); this.activeReviewAbortController = undefined; this.staleReviews++; this.reviewing = false; this.midRunReviewing = false; this.activeReviewId = undefined; this.activeReviewWarning = undefined; } private async reviewDelta(delta: string, timeoutMs: number, options: { correction?: boolean } = {}): Promise { if (this.reviewing || this.disposed) return "stale"; this.reviewing = true; const reviewEpoch = this.epoch; const reviewId = ++this.reviewIdCounter; this.activeReviewId = reviewId; this.activeReviewWarning = undefined; this.status = "reviewing"; let timeout: ReturnType | undefined; const abortController = new AbortController(); this.activeReviewAbortController = abortController; const reviewPromise = Promise.resolve().then(() => this.review({ delta, epoch: reviewEpoch, reviewId, config: this.configResult.config, hasScope: this.scopeBlock().trim().length > 0, signal: abortController.signal, emitWarning: (warning) => this.acceptWarning(reviewEpoch, reviewId, warning), })); try { const result = await Promise.race([ reviewPromise, new Promise<"timeout">((resolve) => { timeout = setTimeout(() => resolve("timeout"), timeoutMs); }), ]); if (result === "timeout") { abortController.abort(); return "timeout"; } if (!this.isCurrent(reviewEpoch, reviewId)) return "stale"; for (const warning of result?.warnings ?? []) this.acceptWarning(reviewEpoch, reviewId, warning); if (result?.stopReason && result.stopReason !== "stop") { this.fail(`Watchdog review ended with stop reason '${result.stopReason}'.`); return "completed"; } this.displayAcceptedReviewWarning(options.correction); return "completed"; } catch (error) { if (this.isCurrent(reviewEpoch, reviewId)) { this.fail(`Watchdog review failed: ${errorMessage(error)}`); return "completed"; } return "stale"; } finally { if (timeout) clearTimeout(timeout); if (this.activeReviewAbortController === abortController) this.activeReviewAbortController = undefined; if (this.epoch === reviewEpoch && this.activeReviewId === reviewId) { this.reviewing = false; this.activeReviewId = undefined; this.activeReviewWarning = undefined; } this.resolveWaiters(this.isSettled()); } } private displayAcceptedReviewWarning(correction = false): void { if (!this.activeReviewWarning) return; const details: WatchdogWarningDetails = { ...this.activeReviewWarning, state: "displayed", displayedAt: new Date().toISOString(), }; this.lastWarning = details; this.displayedWarningSequence++; this.displayWarning?.(details, correction ? { deliverAs: "steer" } : undefined); } private resetAutoFollowState(): void { this.autoFollowQueued = false; this.autoFollowAttempts = 0; this.consecutiveAutoFollowIdentity = undefined; this.consecutiveAutoFollowRepeats = 0; this.autoFollowStalemate = false; } private queueAutoFollowIfNeeded(warning: WatchdogWarningDetails | undefined): void { if (!warning || warning.severity !== "blocker" || warning.stale || !this.configResult.config.autoFollow.blockers || !this.isEnabled()) return; const identity = warning.identity ?? reviewInputSignature([warning.severity, warning.summary, warning.evidence].join("\n")); if (this.consecutiveAutoFollowIdentity === identity) this.consecutiveAutoFollowRepeats++; else { this.consecutiveAutoFollowIdentity = identity; this.consecutiveAutoFollowRepeats = 1; } if (this.consecutiveAutoFollowRepeats >= this.configResult.config.autoFollow.stalemateRepeats) { this.autoFollowStalemate = true; this.lastWarning = { ...warning, state: "stalemate", stalemateRepeats: this.consecutiveAutoFollowRepeats }; return; } const maxAttempts = this.configResult.config.autoFollow.maxAttempts; if (maxAttempts !== null && this.autoFollowAttempts >= maxAttempts) return; if (!this.sendUserMessage) return; this.autoFollowAttempts++; this.autoFollowQueued = true; const prompt = [ "Watchdog auto-follow: address this blocker before continuing.", `Summary: ${warning.summary}`, `Evidence: ${warning.evidence}`, `Recommended action: ${warning.recommendedAction}`, ].join("\n"); this.pendingAutoFollowPrompts.push(prompt); if (this.pendingAutoFollowPrompts.length > 8) this.pendingAutoFollowPrompts.shift(); void Promise.resolve(this.sendUserMessage(prompt)).catch((error) => { this.autoFollowQueued = false; const index = this.pendingAutoFollowPrompts.indexOf(prompt); if (index >= 0) this.pendingAutoFollowPrompts.splice(index, 1); this.lastError = `Watchdog auto-follow failed: ${errorMessage(error)}`; }); } private currentRepoChangeSignature(cwd = this.cwd): WatchdogRepoChangeSignature | undefined { return this.reviewChangesOnly && this.isEnabled() ? this.repoChangeSignature(cwd) : undefined; } private resetRepoChangeBaseline(options: { cwd?: string; reviewed?: boolean } = {}): void { this.turnStartChangeSignature = this.currentRepoChangeSignature(options.cwd ?? this.cwd); if (options.reviewed) this.lastReviewedChangeSignature = this.turnStartChangeSignature?.key; else this.lastReviewedChangeSignature ??= this.turnStartChangeSignature?.key; this.currentChangedPaths = this.turnStartChangeSignature?.changedPaths; this.observedRepoEditThisTurn = false; } private resolveReviewChangeSignature(cwd = this.cwd): WatchdogRepoChangeSignature | undefined { if (!this.reviewChangesOnly) return undefined; const current = this.currentRepoChangeSignature(cwd); if (current) { this.currentChangedPaths = current.changedPaths; if (current.key === this.turnStartChangeSignature?.key) return undefined; if (current.changedPaths.length === 0) return undefined; return current; } return this.observedRepoEditThisTurn ? { root: cwd, key: `observed-edit:${this.epoch}:${this.reviewIdCounter}:${this.pendingDeltaChars}`, changedPaths: [] } : undefined; } private async collectLspDiagnostics(changeSignature: WatchdogRepoChangeSignature | undefined, current: { epoch: number; agentEndId: number; signal: AbortSignal }): Promise { const config = this.configResult.config.lsp; if (!config.enabled || !changeSignature?.changedPaths.length) { this.lastLspSnapshot = { enabled: config.enabled, status: config.enabled ? "skipped" : "disabled", checkedPaths: [], skippedPaths: [], diagnostics: [], diagnosticCount: 0, freshDiagnosticCount: 0, updatedAt: new Date().toISOString(), }; return ""; } try { const raw = await this.lspDiagnostics({ cwd: this.cwd, root: changeSignature.root, changedPaths: changeSignature.changedPaths, config, signal: current.signal, }); if (!this.isAgentEndCurrent(current.epoch, current.agentEndId)) return ""; const diagnosticCount = raw.diagnostics.length; const fresh = this.lspLedger.reduce(raw); this.lastLspSnapshot = { ...fresh, enabled: true, diagnosticCount, freshDiagnosticCount: fresh.diagnostics.length, updatedAt: new Date().toISOString(), }; const warning = watchdogWarningFromLspDiagnostics(fresh); if (warning) this.displayBoundaryWarning(warning); return formatWatchdogLspDiagnosticsBlock(fresh); } catch (error) { if (!this.isAgentEndCurrent(current.epoch, current.agentEndId)) return ""; this.lastLspSnapshot = { enabled: true, status: "failed", checkedPaths: [], skippedPaths: changeSignature.changedPaths, diagnostics: [], diagnosticCount: 0, freshDiagnosticCount: 0, message: `LSP diagnostics failed: ${errorMessage(error)}`, updatedAt: new Date().toISOString(), }; return ""; } } private lspSnapshot(): WatchdogLspRuntimeSnapshot { if (this.lastLspSnapshot) return { ...this.lastLspSnapshot, checkedPaths: [...this.lastLspSnapshot.checkedPaths], skippedPaths: [...this.lastLspSnapshot.skippedPaths], diagnostics: [...this.lastLspSnapshot.diagnostics], }; const enabled = this.isEnabled() && this.configResult.config.lsp.enabled; return { enabled, status: enabled ? "skipped" : "disabled", checkedPaths: [], skippedPaths: [], diagnostics: [], diagnosticCount: 0, freshDiagnosticCount: 0, }; } private appendBoundedDelta(delta: string): void { let entry = delta.trim(); if (!entry) return; if (entry.length > MAX_REVIEW_INPUT_CHARS) entry = entry.slice(-MAX_REVIEW_INPUT_CHARS); this.pendingDeltas.push(entry); this.pendingDeltaChars += entry.length; while (this.pendingDeltas.length > 1 && this.pendingDeltaChars + (this.pendingDeltas.length - 1) * REVIEW_DELTA_SEPARATOR.length > MAX_REVIEW_INPUT_CHARS) { const removed = this.pendingDeltas.shift(); if (removed) this.pendingDeltaChars -= removed.length; } } private buildReviewInput(changeSignature?: WatchdogRepoChangeSignature, lspBlock = ""): string { const input = this.pendingDeltas.join(REVIEW_DELTA_SEPARATOR); const scopeBlock = this.scopeBlock(); const changes = changeSignature?.changedPaths.length ? ["Changed repo paths:", ...changeSignature.changedPaths.slice(0, 200).map((file) => `- ${file}`)].join("\n") : ""; const contextPieces = [scopeBlock, changes, lspBlock].filter(Boolean); if (!contextPieces.length) return input.length > MAX_REVIEW_INPUT_CHARS ? input.slice(-MAX_REVIEW_INPUT_CHARS) : input; const maxContextLength = Math.floor(MAX_REVIEW_INPUT_CHARS / 2); const maxPieceLength = Math.max(1_000, Math.floor(maxContextLength / contextPieces.length)); const boundedContext = contextPieces.map((piece) => piece.length > maxPieceLength ? `${piece.slice(0, maxPieceLength - 6)}\n- ...` : piece).join(REVIEW_DELTA_SEPARATOR); const separatorLength = input ? REVIEW_DELTA_SEPARATOR.length : 0; const inputBudget = MAX_REVIEW_INPUT_CHARS - boundedContext.length - separatorLength; const boundedInput = inputBudget <= 0 ? "" : input.length > inputBudget ? input.slice(-inputBudget) : input; return [boundedContext, boundedInput].filter(Boolean).join(REVIEW_DELTA_SEPARATOR); } private scopeBlock(): string { return this.configResult.config.scope.enabled ? this.scope.render() : ""; } private clearPendingDeltas(): void { this.pendingDeltas = []; this.pendingDeltaChars = 0; } private fail(message: string): void { this.failedReviews++; this.lastError = message; this.status = "failed"; this.clearPendingDeltas(); this.resolveWaiters(true); } private markLastWarningStale(): void { if (!this.lastWarning || this.lastWarning.state === "displayed") return; this.lastWarning = { ...this.lastWarning, stale: true, state: "stale" }; } private isSettled(): boolean { return !this.reviewing && this.pendingDeltas.length === 0; } private waitForSettled(timeoutMs: number): Promise { if (this.isSettled()) return Promise.resolve(true); return new Promise((resolve) => { const waiter: Waiter = { resolve, timer: setTimeout(() => { this.waiters = this.waiters.filter((entry) => entry !== waiter); resolve(false); }, timeoutMs), }; this.waiters.push(waiter); }); } private resolveWaiters(settled: boolean): void { if (!settled && !this.disposed) return; const waiters = this.waiters; this.waiters = []; for (const waiter of waiters) { clearTimeout(waiter.timer); waiter.resolve(settled); } } }