1
0
Fork 0

pi: update herdr integration to v8

This commit is contained in:
Rain 2026-08-22 22:21:34 -04:00
parent 5b5ae288b4
commit 5e8a002b32

View file

@ -2,13 +2,15 @@
// managed by herdr; reinstalling or updating the integration overwrites this file. // managed by herdr; reinstalling or updating the integration overwrites this file.
// add custom hooks/plugins beside this file instead of editing it. // add custom hooks/plugins beside this file instead of editing it.
// HERDR_INTEGRATION_ID=pi // HERDR_INTEGRATION_ID=pi
// HERDR_INTEGRATION_VERSION=4 // HERDR_INTEGRATION_VERSION=8
// @ts-nocheck // @ts-nocheck
import { createConnection } from "node:net"; import net from "node:net";
const HERDR_ENV = process.env.HERDR_ENV; const HERDR_ENV = process.env.HERDR_ENV;
const socketPath = process.env.HERDR_SOCKET_PATH; const socketPath = process.env.HERDR_SOCKET_PATH;
const socketEndpoint =
process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath;
const paneId = process.env.HERDR_PANE_ID; const paneId = process.env.HERDR_PANE_ID;
const source = "herdr:pi"; const source = "herdr:pi";
@ -34,7 +36,7 @@ function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolea
resolve(delivered); resolve(delivered);
}; };
const socket = createConnection(socketPath!); const socket = net.createConnection(socketEndpoint!);
socket.on("error", () => finish(false)); socket.on("error", () => finish(false));
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`)); socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
socket.on("data", () => finish(true)); socket.on("data", () => finish(true));
@ -59,10 +61,6 @@ type QueuedState = {
seq: number; 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 reportSeq = Date.now() * 1000;
let currentAgentSessionId: string | undefined; let currentAgentSessionId: string | undefined;
let currentAgentSessionPath: string | undefined; let currentAgentSessionPath: string | undefined;
@ -72,18 +70,6 @@ function nextReportSeq(): number {
return reportSeq; 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 { function updateSessionRef(ctx: any): void {
try { try {
const file = ctx?.sessionManager?.getSessionFile?.(); const file = ctx?.sessionManager?.getSessionFile?.();
@ -121,7 +107,7 @@ function currentSessionRef(): Record<string, unknown> | undefined {
return undefined; return undefined;
} }
function reportSession(): Promise<void> { function reportSession(sessionStartSource?: string): Promise<void> {
const sessionRef = currentSessionRef(); const sessionRef = currentSessionRef();
if (!sessionRef) { if (!sessionRef) {
return Promise.resolve(); return Promise.resolve();
@ -135,6 +121,7 @@ function reportSession(): Promise<void> {
source, source,
agent: "pi", agent: "pi",
seq: nextReportSeq(), seq: nextReportSeq(),
session_start_source: sessionStartSource,
...sessionRef, ...sessionRef,
}, },
}); });
@ -155,29 +142,6 @@ function sendState(state: AgentState, message?: string, seq = nextReportSeq()):
}); });
} }
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: "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 sendInFlight = false;
let queuedState: QueuedState | undefined; let queuedState: QueuedState | undefined;
@ -208,74 +172,23 @@ async function drainStateQueue(): Promise<void> {
} }
} }
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) { export default function (pi) {
if (!enabled()) { if (!enabled()) {
return; return;
} }
let agentActive = false; let agentActive = false;
let retryHoldActive = false;
let failureBlocked = false;
let failureMessage: string | undefined;
let blockedCount = 0; let blockedCount = 0;
let blockedMessage: string | undefined; let blockedMessage: string | undefined;
let lastState: AgentState | undefined; let lastState: AgentState | undefined;
let lastMessage: string | undefined; let lastMessage: string | undefined;
let idleTimer: ReturnType<typeof setTimeout> | undefined;
let retryTimer: ReturnType<typeof setTimeout> | undefined;
let rootSession = false; let rootSession = false;
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() { function desiredState() {
if (blockedCount > 0) { if (blockedCount > 0) {
return { state: "blocked" as const, message: blockedMessage }; return { state: "blocked" as const, message: blockedMessage };
} }
if (failureBlocked) { if (agentActive) {
return { state: "blocked" as const, message: failureMessage };
}
if (agentActive || retryHoldActive) {
return { state: "working" as const, message: undefined }; return { state: "working" as const, message: undefined };
} }
return { state: "idle" as const, message: undefined }; return { state: "idle" as const, message: undefined };
@ -291,32 +204,6 @@ export default function (pi) {
queueState(next.state, 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) => { pi.events.on("herdr:blocked", (data) => {
if (!rootSession) { if (!rootSession) {
return; return;
@ -330,19 +217,20 @@ export default function (pi) {
return; return;
} }
clearPendingTimers();
blockedCount += 1; blockedCount += 1;
blockedMessage = data.label; blockedMessage = data.label;
publishState(); publishState();
}); });
pi.on("session_start", (_event, ctx) => { pi.on("session_start", async (event, ctx) => {
if (ctx?.hasUI !== true) { // 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; return;
} }
rootSession = true; rootSession = true;
updateSessionRef(ctx); updateSessionRef(ctx);
void reportSession(); await reportSession(event?.reason);
// A reload can replace this extension mid-run without emitting another agent_start. // A reload can replace this extension mid-run without emitting another agent_start.
agentActive = ctx?.isIdle?.() === false; agentActive = ctx?.isIdle?.() === false;
publishState(true); publishState(true);
@ -354,41 +242,16 @@ export default function (pi) {
} }
updateSessionRef(ctx); updateSessionRef(ctx);
void reportSession(); void reportSession();
clearPendingTimers();
clearFailureState();
agentActive = true; agentActive = true;
publishState(); publishState();
}); });
pi.on("agent_end", (event) => { pi.on("agent_settled", (_event, ctx) => {
if (!rootSession) { if (!rootSession || ctx?.isIdle?.() !== true) {
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; return;
} }
agentActive = false; agentActive = false;
publishState();
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();
}
}); });
} }