pi: update herdr integration to v8
This commit is contained in:
parent
5b5ae288b4
commit
5e8a002b32
1 changed files with 16 additions and 153 deletions
|
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue