fix(pi): preserve in-flight blocked transitions

refs #2971
This commit is contained in:
akbash-bot
2026-08-19 05:11:42 +00:00
parent a5c69beabf
commit eac2b8e73f
5 changed files with 58 additions and 9 deletions
+1
View File
@@ -26,6 +26,7 @@
- Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay.
### Fixed
- The Pi integration now preserves short blocked transitions while an earlier lifecycle report is still in flight. (#2971)
- High-rate output from many hidden panes no longer floods the server loop with redundant wakeups, and terminal input-mode synchronization no longer formats pane scrollback to read one keyboard flag.
- Chinese IME commits now reach panes on macOS when the focused application requests printable key-release events. (#2924)
- Windows now recognizes `Ctrl+1` through `Ctrl+9` keybindings instead of decoding those key records as control characters. (#2910)
@@ -315,6 +315,52 @@ test("Pi settlement preserves explicit blocked-state precedence", async () => {
expect(requestStates(requests)).toEqual(["idle", "working", "blocked", "idle"]);
});
test("Pi preserves a blocked transition while a state report is in flight", async () => {
const recordingSocketPath = join(tmpdir(), `herdr-pi-blocked-order-${process.pid}.sock`);
socketPath = recordingSocketPath;
await rm(recordingSocketPath, { force: true });
const requests: unknown[] = [];
let acknowledgeFirstReport: (() => void) | undefined;
const recordingServer = createServer((socket) => {
let input = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
input += chunk;
const newline = input.indexOf("\n");
if (newline === -1) {
return;
}
requests.push(JSON.parse(input.slice(0, newline)));
if (requestStates(requests).length === 1) {
acknowledgeFirstReport = () => socket.end("{}\n");
return;
}
socket.end("{}\n");
});
});
server = recordingServer;
await new Promise<void>((resolve, reject) => {
recordingServer.once("error", reject);
recordingServer.listen(recordingSocketPath, resolve);
});
configureIntegrationEnvironment(recordingSocketPath);
const { eventHandlers, handlers, pi } = createExtensionHarness();
const { default: install } = await importFresh("./pi/herdr-agent-state.ts");
install(pi);
await handlers.get("session_start")?.({ reason: "startup" }, piContext(() => false));
await waitFor(() => acknowledgeFirstReport !== undefined);
eventHandlers.get("herdr:blocked")?.({ active: true, label: "approval" }, {});
eventHandlers.get("herdr:blocked")?.({ active: false }, {});
acknowledgeFirstReport?.();
await waitFor(() => requestStates(requests).length === 3);
expect(requestStates(requests)).toEqual(["working", "blocked", "working"]);
});
test("Pi reports the session replacement source", async () => {
const requests = await startRecordingServer("pi-session-source");
const { handlers, pi } = createExtensionHarness();
@@ -2,7 +2,7 @@
// 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=8
// HERDR_INTEGRATION_VERSION=9
// @ts-nocheck
import net from "node:net";
@@ -143,10 +143,10 @@ function sendState(state: AgentState, message?: string, seq = nextReportSeq()):
}
let sendInFlight = false;
let queuedState: QueuedState | undefined;
const queuedStates: QueuedState[] = [];
function queueState(state: AgentState, message?: string): void {
queuedState = { state, message, seq: nextReportSeq() };
queuedStates.push({ state, message, seq: nextReportSeq() });
if (!sendInFlight) {
void drainStateQueue();
}
@@ -159,14 +159,16 @@ async function drainStateQueue(): Promise<void> {
sendInFlight = true;
try {
while (queuedState) {
const next = queuedState;
queuedState = undefined;
while (true) {
const next = queuedStates.shift();
if (!next) {
break;
}
await sendState(next.state, next.message, next.seq);
}
} finally {
sendInFlight = false;
if (queuedState) {
if (queuedStates.length > 0) {
void drainStateQueue();
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ pub(crate) use types::{IntegrationRecommendation, IntegrationStatus, Integration
const PI_EXTENSION_INSTALL_NAME: &str = "herdr-agent-state.ts";
const PI_EXTENSION_ASSET: &str = include_str!("assets/pi/herdr-agent-state.ts");
const PI_INTEGRATION_VERSION: u32 = 8;
const PI_INTEGRATION_VERSION: u32 = 9;
const OMP_EXTENSION_INSTALL_NAME: &str = "herdr-omp-agent-state.ts";
const OMP_EXTENSION_ASSET: &str = include_str!("assets/omp/herdr-agent-state.ts");
const OMP_INTEGRATION_VERSION: u32 = 8;
+1 -1
View File
@@ -293,7 +293,7 @@ fn integration_commands_run_locally_when_server_is_missing() {
.unwrap();
assert_eq!(integration_status.status.code(), Some(0));
let status_stdout = String::from_utf8_lossy(&integration_status.stdout);
assert!(status_stdout.contains("pi: current (v8)"));
assert!(status_stdout.contains("pi: current (v9)"));
assert!(status_stdout.contains("claude: not installed"));
let integration_uninstall = Command::new(env!("CARGO_BIN_EXE_herdr"))