refactor(hooks): remove pulse-monitor hook
🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -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<typeof setInterval> | 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<string, any> | 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
createSessionRecoveryHook,
|
createSessionRecoveryHook,
|
||||||
createCommentCheckerHooks,
|
createCommentCheckerHooks,
|
||||||
createGrepOutputTruncatorHook,
|
createGrepOutputTruncatorHook,
|
||||||
createPulseMonitorHook,
|
|
||||||
createDirectoryAgentsInjectorHook,
|
createDirectoryAgentsInjectorHook,
|
||||||
createEmptyTaskResponseDetectorHook,
|
createEmptyTaskResponseDetectorHook,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
@@ -54,7 +53,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
||||||
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
||||||
const sessionRecovery = createSessionRecoveryHook(ctx);
|
const sessionRecovery = createSessionRecoveryHook(ctx);
|
||||||
const pulseMonitor = createPulseMonitorHook(ctx);
|
|
||||||
const commentChecker = createCommentCheckerHooks();
|
const commentChecker = createCommentCheckerHooks();
|
||||||
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
||||||
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
||||||
@@ -93,7 +91,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
event: async (input) => {
|
event: async (input) => {
|
||||||
await todoContinuationEnforcer(input);
|
await todoContinuationEnforcer(input);
|
||||||
await contextWindowMonitor.event(input);
|
await contextWindowMonitor.event(input);
|
||||||
await pulseMonitor.event(input);
|
|
||||||
await directoryAgentsInjector.event(input);
|
await directoryAgentsInjector.event(input);
|
||||||
|
|
||||||
const { event } = input;
|
const { event } = input;
|
||||||
@@ -194,7 +191,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tool.execute.before": async (input, output) => {
|
"tool.execute.before": async (input, output) => {
|
||||||
await pulseMonitor["tool.execute.before"]();
|
|
||||||
await commentChecker["tool.execute.before"](input, output);
|
await commentChecker["tool.execute.before"](input, output);
|
||||||
|
|
||||||
if (input.sessionID === mainSessionID) {
|
if (input.sessionID === mainSessionID) {
|
||||||
@@ -209,7 +205,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tool.execute.after": async (input, output) => {
|
"tool.execute.after": async (input, output) => {
|
||||||
await pulseMonitor["tool.execute.after"](input);
|
|
||||||
await grepOutputTruncator["tool.execute.after"](input, output);
|
await grepOutputTruncator["tool.execute.after"](input, output);
|
||||||
await contextWindowMonitor["tool.execute.after"](input, output);
|
await contextWindowMonitor["tool.execute.after"](input, output);
|
||||||
await commentChecker["tool.execute.after"](input, output);
|
await commentChecker["tool.execute.after"](input, output);
|
||||||
|
|||||||
Reference in New Issue
Block a user