From 5e8a002b32f7731882d9ed666ccb72c4e769d8d5 Mon Sep 17 00:00:00 2001 From: rain Date: Sat, 22 Aug 2026 22:21:34 -0400 Subject: [PATCH] pi: update herdr integration to v8 --- dot_pi/agent/extensions/herdr-agent-state.ts | 169 ++----------------- 1 file changed, 16 insertions(+), 153 deletions(-) diff --git a/dot_pi/agent/extensions/herdr-agent-state.ts b/dot_pi/agent/extensions/herdr-agent-state.ts index 9048911..67bc418 100644 --- a/dot_pi/agent/extensions/herdr-agent-state.ts +++ b/dot_pi/agent/extensions/herdr-agent-state.ts @@ -2,13 +2,15 @@ // 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 +// HERDR_INTEGRATION_VERSION=8 // @ts-nocheck -import { createConnection } from "node:net"; +import net from "node:net"; const HERDR_ENV = process.env.HERDR_ENV; const socketPath = process.env.HERDR_SOCKET_PATH; +const socketEndpoint = + process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath; const paneId = process.env.HERDR_PANE_ID; const source = "herdr:pi"; @@ -34,7 +36,7 @@ function sendRequestAttempt(request: unknown, timeoutMs: number): Promise finish(false)); socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`)); socket.on("data", () => finish(true)); @@ -59,10 +61,6 @@ type QueuedState = { 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; @@ -72,18 +70,6 @@ function nextReportSeq(): number { 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?.(); @@ -121,7 +107,7 @@ function currentSessionRef(): Record | undefined { return undefined; } -function reportSession(): Promise { +function reportSession(sessionStartSource?: string): Promise { const sessionRef = currentSessionRef(); if (!sessionRef) { return Promise.resolve(); @@ -135,6 +121,7 @@ function reportSession(): Promise { source, agent: "pi", seq: nextReportSeq(), + session_start_source: sessionStartSource, ...sessionRef, }, }); @@ -155,29 +142,6 @@ function sendState(state: AgentState, message?: string, seq = nextReportSeq()): }); } -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; @@ -208,74 +172,23 @@ async function drainStateQueue(): Promise { } } -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) { + if (agentActive) { return { state: "working" as const, message: undefined }; } return { state: "idle" as const, message: undefined }; @@ -291,32 +204,6 @@ export default function (pi) { 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; @@ -330,19 +217,20 @@ export default function (pi) { return; } - clearPendingTimers(); blockedCount += 1; blockedMessage = data.label; publishState(); }); - pi.on("session_start", (_event, ctx) => { - if (ctx?.hasUI !== true) { + pi.on("session_start", async (event, ctx) => { + // TUI only: RPC/JSON/print modes are headless (no PTY herdr can display), + // and RPC still reports hasUI=true, so mode is the reliable gate. + if (ctx?.mode !== "tui") { return; } rootSession = true; updateSessionRef(ctx); - void reportSession(); + await reportSession(event?.reason); // A reload can replace this extension mid-run without emitting another agent_start. agentActive = ctx?.isIdle?.() === false; publishState(true); @@ -354,41 +242,16 @@ export default function (pi) { } 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. + pi.on("agent_settled", (_event, ctx) => { + if (!rootSession || ctx?.isIdle?.() !== true) { 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(); - } + publishState(); }); }