From bacafdf715918429c4760ca0c6295d9df8eaba03 Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 21 Aug 2026 17:29:47 -0400 Subject: [PATCH] fold omp runtime config and remaining pi extensions into source --- dot_omp/private_agent/mcp.json | 13 +- dot_omp/private_agent/private_config.yml | 8 +- dot_pi/agent/extensions/herdr-agent-state.ts | 394 +++++++++++++++++++ dot_pi/agent/extensions/private_notify.ts | 37 ++ 4 files changed, 446 insertions(+), 6 deletions(-) create mode 100644 dot_pi/agent/extensions/herdr-agent-state.ts create mode 100644 dot_pi/agent/extensions/private_notify.ts diff --git a/dot_omp/private_agent/mcp.json b/dot_omp/private_agent/mcp.json index 5525ba0..c92581f 100644 --- a/dot_omp/private_agent/mcp.json +++ b/dot_omp/private_agent/mcp.json @@ -3,11 +3,20 @@ "mcpServers": { "firecrawl": { "command": "npx", - "args": ["-y", "firecrawl-mcp"], + "args": [ + "-y", + "firecrawl-mcp" + ], "env": { "FIRECRAWL_API_KEY": "123", "FIRECRAWL_API_URL": "http://bazzite.local:3002" } + }, + "godot-mcp-pro": { + "command": "node", + "args": [ + "/home/melon/Desktop/godot-mcp-pro-v1.16.0/server/build/index.js" + ] } } -} +} \ No newline at end of file diff --git a/dot_omp/private_agent/private_config.yml b/dot_omp/private_agent/private_config.yml index e12d5fe..98c3579 100644 --- a/dot_omp/private_agent/private_config.yml +++ b/dot_omp/private_agent/private_config.yml @@ -29,18 +29,18 @@ symbolPreset: nerd theme: dark: dark-gruvbox light: light -setupVersion: 1 +setupVersion: 2 modelRoles: - task: llama-swap.byte/qwen36-27b-711 + task: llama-swap.miche/godoter-27b plan: zai-coding/glm-5.2:xhigh slow: deepseek/deepseek-v4-pro:high vision: llama-swap.byte/qwen36-35b-heretic-apex tiny: llama-swap.byte/gemma4-26b-hauhau - default: deepseek/deepseek-v4-flash:high smol: llama-swap.byte/kat-coder-v2.5-dev commit: llama-swap.byte/gemma4-26b-hauhau designer: zai-coding/glm-5.2 advisor: zai-coding/glm-5.2 + default: openrouter/stealth/ox-alpha:max retry: fallbackChains: default: @@ -73,5 +73,5 @@ tui: textSizing: false defaultThinkingLevel: auto personality: pragmatic -hideThinkingBlock: false +hideThinkingBlock: true readLineNumbers: true \ No newline at end of file diff --git a/dot_pi/agent/extensions/herdr-agent-state.ts b/dot_pi/agent/extensions/herdr-agent-state.ts new file mode 100644 index 0000000..9048911 --- /dev/null +++ b/dot_pi/agent/extensions/herdr-agent-state.ts @@ -0,0 +1,394 @@ +// installed by herdr +// managed by herdr; reinstalling or updating the integration overwrites this file. +// add custom hooks/plugins beside this file instead of editing it. +// HERDR_INTEGRATION_ID=pi +// HERDR_INTEGRATION_VERSION=4 +// @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:pi"; + +function enabled() { + return HERDR_ENV === "1" && !!socketPath && !!paneId; +} + +function sendRequestAttempt(request: unknown, timeoutMs: number): Promise { + if (!enabled()) { + return Promise.resolve(true); + } + + return new Promise((resolve) => { + let done = false; + let timeout: ReturnType | undefined; + const finish = (delivered: boolean) => { + if (done) return; + done = true; + if (timeout) { + clearTimeout(timeout); + } + socket.destroy(); + resolve(delivered); + }; + + const socket = createConnection(socketPath!); + socket.on("error", () => finish(false)); + socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`)); + socket.on("data", () => finish(true)); + socket.on("end", () => finish(false)); + timeout = setTimeout(() => finish(false), timeoutMs); + timeout.unref?.(); + }); +} + +async function sendRequest(request: unknown): Promise { + if (await sendRequestAttempt(request, 500)) { + return; + } + await sendRequestAttempt(request, 1500); +} + +type AgentState = "working" | "blocked" | "idle"; + +type QueuedState = { + state: AgentState; + message?: string; + seq: number; +}; + +const idleDebounceMs = parseDurationEnv("HERDR_PI_IDLE_DEBOUNCE_MS", 250); +const retryGraceMs = parseDurationEnv("HERDR_PI_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; +let currentAgentSessionId: string | undefined; +let currentAgentSessionPath: string | undefined; + +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 updateSessionRef(ctx: any): void { + try { + const file = ctx?.sessionManager?.getSessionFile?.(); + currentAgentSessionPath = + typeof file === "string" && file.startsWith("/") ? file : undefined; + } catch { + currentAgentSessionPath = undefined; + } + + try { + const id = ctx?.sessionManager?.getSessionId?.(); + currentAgentSessionId = typeof id === "string" && id.length > 0 ? id : undefined; + } catch { + currentAgentSessionId = undefined; + } +} + +function withSessionRef(params: Record): Record { + if (currentAgentSessionPath) { + return { ...params, agent_session_path: currentAgentSessionPath }; + } + if (currentAgentSessionId) { + return { ...params, agent_session_id: currentAgentSessionId }; + } + return params; +} + +function currentSessionRef(): Record | undefined { + if (currentAgentSessionPath) { + return { agent_session_path: currentAgentSessionPath }; + } + if (currentAgentSessionId) { + return { agent_session_id: currentAgentSessionId }; + } + return undefined; +} + +function reportSession(): Promise { + const sessionRef = currentSessionRef(); + if (!sessionRef) { + return Promise.resolve(); + } + + return sendRequest({ + id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent_session", + params: { + pane_id: paneId, + source, + agent: "pi", + seq: nextReportSeq(), + ...sessionRef, + }, + }); +} + +function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise { + return sendRequest({ + id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent", + params: withSessionRef({ + pane_id: paneId, + source, + agent: "pi", + state, + message, + seq, + }), + }); +} + +function releaseAgent(): Promise { + return sendRequest({ + id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.release_agent", + params: { + pane_id: paneId, + source, + agent: "pi", + seq: nextReportSeq(), + }, + }); +} + +function shouldReleaseOnSessionShutdown(event: any): boolean { + // Pi tears down and rebinds extension runtimes for internal lifecycle actions + // such as /reload, /new, /resume, and /fork. Those do not mean the pane's + // agent process has exited, and releasing hook authority there can suppress + // legitimate reports from the replacement runtime. Only a user/process quit + // should release Herdr's full-lifecycle authority. + const reason = event?.reason; + return reason === "quit"; +} + +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 { + 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"; +} + +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 | undefined; + let retryTimer: ReturnType | undefined; + let rootSession = false; + + function clearTimer(timer: ReturnType | 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(force = false) { + const next = desiredState(); + if (!force && 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 (!rootSession) { + return; + } + 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", (_event, ctx) => { + if (ctx?.hasUI !== true) { + return; + } + rootSession = true; + updateSessionRef(ctx); + void reportSession(); + // A reload can replace this extension mid-run without emitting another agent_start. + agentActive = ctx?.isIdle?.() === false; + publishState(true); + }); + + pi.on("agent_start", (_event, ctx) => { + if (!rootSession) { + return; + } + updateSessionRef(ctx); + void reportSession(); + clearPendingTimers(); + clearFailureState(); + agentActive = true; + publishState(); + }); + + pi.on("agent_end", (event) => { + if (!rootSession) { + return; + } + if (!agentActive) { + // Pi 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 (event) => { + if (!rootSession) { + return; + } + clearPendingTimers(); + if (shouldReleaseOnSessionShutdown(event)) { + await releaseAgent(); + } + }); +} diff --git a/dot_pi/agent/extensions/private_notify.ts b/dot_pi/agent/extensions/private_notify.ts new file mode 100644 index 0000000..e21a67a --- /dev/null +++ b/dot_pi/agent/extensions/private_notify.ts @@ -0,0 +1,37 @@ +/** + * Pi Notify Extension (with audible bell) + * + * Fires a terminal bell + native desktop notification when Pi finishes. + * - Bell (\x07): tmux flags the window in the status bar + audible ding + * - OSC 777: Ghostty, iTerm2, WezTerm desktop notification popup + * - OSC 99: Kitty desktop notification popup + * + * Based on the official example from earendil-works/pi, with bell added. + */ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +function notifyOSC777(title: string, body: string): void { + process.stdout.write(`\x1b]777;notify;${title};${body}\x07`); +} + +function notifyOSC99(title: string, body: string): void { + process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`); + process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`); +} + +function notify(title: string, body: string): void { + // Audible bell — tmux catches this, flags the window, terminal dings + process.stdout.write("\x07"); + // Desktop notification popup (OSC 777 for Ghostty, OSC 99 for Kitty) + if (process.env.KITTY_WINDOW_ID) { + notifyOSC99(title, body); + } else { + notifyOSC777(title, body); + } +} + +export default function (pi: ExtensionAPI) { + pi.on("agent_end", async () => { + notify("Pi", "Task complete — ready for input"); + }); +}