diff --git a/src/hooks/pulse-monitor.ts b/src/hooks/pulse-monitor.ts deleted file mode 100644 index b53e9d0..0000000 --- a/src/hooks/pulse-monitor.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { PluginInput } from "@opencode-ai/plugin" - -export function createPulseMonitorHook(ctx: PluginInput) { - const STANDARD_TIMEOUT = 5 * 60 * 1000 // 5 minutes - const THINKING_TIMEOUT = 5 * 60 * 1000 // 5 minutes - const CHECK_INTERVAL = 5 * 1000 // 5 seconds - - let lastHeartbeat = Date.now() - let isMonitoring = false - let currentSessionID: string | null = null - let monitorTimer: ReturnType | null = null - let isThinking = false - - const startMonitoring = (sessionID: string) => { - if (currentSessionID !== sessionID) { - currentSessionID = sessionID - // Reset thinking state when switching sessions or starting new - isThinking = false - } - - lastHeartbeat = Date.now() - - if (!isMonitoring) { - isMonitoring = true - if (monitorTimer) clearInterval(monitorTimer) - - monitorTimer = setInterval(async () => { - if (!isMonitoring || !currentSessionID) return - - const timeSinceLastHeartbeat = Date.now() - lastHeartbeat - const currentTimeout = isThinking ? THINKING_TIMEOUT : STANDARD_TIMEOUT - - if (timeSinceLastHeartbeat > currentTimeout) { - await recoverStalledSession(currentSessionID, timeSinceLastHeartbeat, isThinking) - } - }, CHECK_INTERVAL) - } - } - - const stopMonitoring = () => { - isMonitoring = false - if (monitorTimer) { - clearInterval(monitorTimer) - monitorTimer = null - } - } - - const updateHeartbeat = (isThinkingUpdate?: boolean) => { - if (isMonitoring) { - lastHeartbeat = Date.now() - if (isThinkingUpdate !== undefined) { - isThinking = isThinkingUpdate - } - } - } - - const recoverStalledSession = async (sessionID: string, stalledDuration: number, wasThinking: boolean) => { - stopMonitoring() - - try { - const durationSec = Math.round(stalledDuration/1000) - const typeStr = wasThinking ? "Thinking" : "Standard" - - // 1. Notify User - await ctx.client.tui.showToast({ - body: { - title: "Pulse Monitor: Cardiac Arrest", - message: `Session stalled (${typeStr}) for ${durationSec}s. Defibrillating...`, - variant: "error", - duration: 5000 - } - }).catch(() => {}) - - // 2. Abort current generation (Defibrillation shock) - await ctx.client.session.abort({ path: { id: sessionID } }).catch(() => {}) - - // 3. Wait a bit for state to settle - await new Promise(resolve => setTimeout(resolve, 1500)) - - // 4. Prompt "continue" to kickstart (CPR) - await ctx.client.session.prompt({ - path: { id: sessionID }, - body: { parts: [{ type: "text", text: "The connection was unstable and stalled. Please continue from where you left off." }] }, - query: { directory: ctx.directory } - }) - - // Resume monitoring - startMonitoring(sessionID) - - } catch (err) { - console.error("[PulseMonitor] Recovery failed:", err) - // If recovery fails, we stop monitoring to avoid loops - stopMonitoring() - } - } - - return { - event: async (input: { event: any }) => { - const { event } = input - const props = event.properties as Record | undefined - - // Monitor both session updates and part updates to capture token flow - if (event.type === "session.updated" || event.type === "message.part.updated") { - // Try to get sessionID from various common locations - const sessionID = props?.info?.id || props?.sessionID - - if (sessionID) { - if (!isMonitoring) startMonitoring(sessionID) - - // Check for thinking indicators in the payload - let thinkingUpdate: boolean | undefined = undefined - - if (event.type === "message.part.updated") { - const part = props?.part - if (part) { - const THINKING_TYPES = ["thinking", "redacted_thinking", "reasoning"] - if (THINKING_TYPES.includes(part.type)) { - thinkingUpdate = true - } else if (part.type === "text" || part.type === "tool_use") { - thinkingUpdate = false - } - } - } - - updateHeartbeat(thinkingUpdate) - } - } else if (event.type === "session.idle" || event.type === "session.error" || event.type === "session.stopped") { - stopMonitoring() - } - }, - "tool.execute.before": async () => { - // Pause monitoring while tool runs locally (tools can take time) - stopMonitoring() - }, - "tool.execute.after": async (input: { sessionID: string }) => { - // Reset heartbeat after tool execution to prevent false positives - // Tools can take arbitrary time, so we need a fresh baseline - if (input.sessionID && currentSessionID === input.sessionID) { - lastHeartbeat = Date.now() - } - // Don't forcefully restart monitoring here - // Monitoring will naturally resume when next session/message event arrives - // This prevents stalled detection on legitimately idle sessions - } - } -} diff --git a/src/index.ts b/src/index.ts index 94a0141..b654e78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ import { createSessionRecoveryHook, createCommentCheckerHooks, createGrepOutputTruncatorHook, - createPulseMonitorHook, createDirectoryAgentsInjectorHook, createEmptyTaskResponseDetectorHook, } from "./hooks"; @@ -54,7 +53,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx); const contextWindowMonitor = createContextWindowMonitorHook(ctx); const sessionRecovery = createSessionRecoveryHook(ctx); - const pulseMonitor = createPulseMonitorHook(ctx); const commentChecker = createCommentCheckerHooks(); const grepOutputTruncator = createGrepOutputTruncatorHook(ctx); const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx); @@ -93,7 +91,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { event: async (input) => { await todoContinuationEnforcer(input); await contextWindowMonitor.event(input); - await pulseMonitor.event(input); await directoryAgentsInjector.event(input); const { event } = input; @@ -194,7 +191,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }, "tool.execute.before": async (input, output) => { - await pulseMonitor["tool.execute.before"](); await commentChecker["tool.execute.before"](input, output); if (input.sessionID === mainSessionID) { @@ -209,7 +205,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }, "tool.execute.after": async (input, output) => { - await pulseMonitor["tool.execute.after"](input); await grepOutputTruncator["tool.execute.after"](input, output); await contextWindowMonitor["tool.execute.after"](input, output); await commentChecker["tool.execute.after"](input, output);