diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-23 15:04:14 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-23 15:19:36 +0300 |
| commit | ed11779fcaebea7cc3223c4e221ea43674057891 (patch) | |
| tree | 13f2962445f60252ef9bb77a44c2033f7f67b5bb /src/integration/assets/omp | |
| parent | 5954ece9f7e13346a9ae6e7bcabda307280c26ea (diff) | |
feat: add omp integration
Diffstat (limited to 'src/integration/assets/omp')
| -rw-r--r-- | src/integration/assets/omp/herdr-agent-state.ts | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/src/integration/assets/omp/herdr-agent-state.ts b/src/integration/assets/omp/herdr-agent-state.ts new file mode 100644 index 00000000..64c4b02a --- /dev/null +++ b/src/integration/assets/omp/herdr-agent-state.ts @@ -0,0 +1,289 @@ +// installed by herdr +// safe to edit. this integration only activates inside herdr-managed panes. +// HERDR_INTEGRATION_ID=omp +// HERDR_INTEGRATION_VERSION=1 +// @ts-nocheck + +import { createConnection } from "node:net"; + +const HERDR_ENV = process.env.HERDR_ENV; +const socketPath = process.env.HERDR_SOCKET_PATH; +const paneId = process.env.HERDR_PANE_ID; +const source = "herdr:omp"; + +function enabled() { + return HERDR_ENV === "1" && !!socketPath && !!paneId; +} + +function sendRequest(request: unknown): Promise<void> { + if (!enabled()) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + let done = false; + const finish = () => { + if (done) return; + done = true; + socket.destroy(); + resolve(); + }; + + const socket = createConnection(socketPath!); + socket.on("error", finish); + socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`)); + socket.on("data", finish); + socket.on("end", finish); + const timeout = setTimeout(finish, 500); + timeout.unref?.(); + }); +} + +type AgentState = "working" | "blocked" | "idle"; + +type QueuedState = { + state: AgentState; + message?: string; + seq: number; +}; + +const idleDebounceMs = parseDurationEnv("HERDR_OMP_IDLE_DEBOUNCE_MS", 250); +const retryGraceMs = parseDurationEnv("HERDR_OMP_RETRY_GRACE_MS", 2500); +const retryableErrorPattern = + /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i; +let reportSeq = Date.now() * 1000; + +function nextReportSeq(): number { + reportSeq += 1; + return reportSeq; +} + +function parseDurationEnv(name: string, fallback: number): number { + const raw = process.env[name]; + if (!raw) { + return fallback; + } + const parsed = Number.parseInt(raw, 10); + if (!Number.isFinite(parsed) || parsed < 0) { + return fallback; + } + return parsed; +} + +function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise<void> { + return sendRequest({ + id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent", + params: { + pane_id: paneId, + source, + agent: "omp", + state, + message, + seq, + }, + }); +} + +let sendInFlight = false; +let queuedState: QueuedState | undefined; + +function queueState(state: AgentState, message?: string): void { + queuedState = { state, message, seq: nextReportSeq() }; + if (!sendInFlight) { + void drainStateQueue(); + } +} + +async function drainStateQueue(): Promise<void> { + if (sendInFlight) { + return; + } + + sendInFlight = true; + try { + while (queuedState) { + const next = queuedState; + queuedState = undefined; + await sendState(next.state, next.message, next.seq); + } + } finally { + sendInFlight = false; + if (queuedState) { + void drainStateQueue(); + } + } +} + +function lastAssistantMessage(messages: unknown[]): any | undefined { + for (let i = messages.length - 1; i >= 0; i -= 1) { + const message = messages[i] as any; + if (message?.role === "assistant") { + return message; + } + } + return undefined; +} + +function retryableErrorMessage(event: any): string | undefined { + const messages = Array.isArray(event?.messages) ? event.messages : []; + const assistant = lastAssistantMessage(messages); + if (assistant?.stopReason !== "error") { + return undefined; + } + + const errorMessage = String(assistant.errorMessage ?? ""); + if (!retryableErrorPattern.test(errorMessage)) { + return undefined; + } + return errorMessage || "retryable provider error"; +} + +function releaseAgent(): Promise<void> { + return sendRequest({ + id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.release_agent", + params: { + pane_id: paneId, + source, + agent: "omp", + seq: nextReportSeq(), + }, + }); +} + +export default function (pi) { + if (!enabled()) { + return; + } + + let agentActive = false; + let retryHoldActive = false; + let failureBlocked = false; + let failureMessage: string | undefined; + let blockedCount = 0; + let blockedMessage: string | undefined; + let lastState: AgentState | undefined; + let lastMessage: string | undefined; + let idleTimer: ReturnType<typeof setTimeout> | undefined; + let retryTimer: ReturnType<typeof setTimeout> | undefined; + + function clearTimer(timer: ReturnType<typeof setTimeout> | undefined) { + if (timer) { + clearTimeout(timer); + } + } + + function clearPendingTimers() { + clearTimer(idleTimer); + clearTimer(retryTimer); + idleTimer = undefined; + retryTimer = undefined; + } + + function clearFailureState() { + retryHoldActive = false; + failureBlocked = false; + failureMessage = undefined; + } + + function desiredState() { + if (blockedCount > 0) { + return { state: "blocked" as const, message: blockedMessage }; + } + if (failureBlocked) { + return { state: "blocked" as const, message: failureMessage }; + } + if (agentActive || retryHoldActive) { + return { state: "working" as const, message: undefined }; + } + return { state: "idle" as const, message: undefined }; + } + + function publishState() { + const next = desiredState(); + if (next.state === lastState && next.message === lastMessage) { + return; + } + lastState = next.state; + lastMessage = next.message; + queueState(next.state, next.message); + } + + function scheduleIdle() { + clearPendingTimers(); + clearFailureState(); + idleTimer = setTimeout(() => { + idleTimer = undefined; + publishState(); + }, idleDebounceMs); + idleTimer.unref?.(); + } + + function holdForRetry(message: string) { + clearPendingTimers(); + retryHoldActive = true; + failureBlocked = false; + failureMessage = message; + publishState(); + + retryTimer = setTimeout(() => { + retryTimer = undefined; + retryHoldActive = false; + failureBlocked = true; + publishState(); + }, retryGraceMs); + retryTimer.unref?.(); + } + + pi.events.on("herdr:blocked", (data) => { + if (!data?.active) { + blockedCount = Math.max(0, blockedCount - 1); + if (blockedCount === 0) { + blockedMessage = undefined; + } + publishState(); + return; + } + + clearPendingTimers(); + blockedCount += 1; + blockedMessage = data.label; + publishState(); + }); + + pi.on("session_start", () => { + publishState(); + }); + + pi.on("agent_start", () => { + clearPendingTimers(); + clearFailureState(); + agentActive = true; + publishState(); + }); + + pi.on("agent_end", (event) => { + if (!agentActive) { + // OMP can emit duplicate/late end events while auto-retry is already + // holding the pane in Working. Do not let an unqualified duplicate end + // cancel the retry hold and publish a false Idle. + return; + } + + agentActive = false; + + const retryableMessage = retryableErrorMessage(event); + if (retryableMessage) { + holdForRetry(retryableMessage); + return; + } + + scheduleIdle(); + }); + + pi.on("session_shutdown", async () => { + clearPendingTimers(); + await releaseAgent(); + }); +} |
