aboutsummaryrefslogtreecommitdiff
path: root/src/integration
diff options
context:
space:
mode:
authorDillon Mulroy <dillon.mulroy@gmail.com>2026-06-25 10:59:17 -0400
committerGitHub <noreply@github.com>2026-06-25 17:59:17 +0300
commit3c7327c267a352023797c8a13fd0b7f4a7a40591 (patch)
tree5476b4de45d429d2fc245869233f30cb9232f803 /src/integration
parent7a1578d9493b2a36cc765bdb96db99cca8bae643 (diff)
fix: avoid pi release on internal session reloads (#800)
* fix: avoid pi release on internal session reloads * fix: align omp session shutdown release with pi --------- Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/integration')
-rw-r--r--src/integration/assets/omp/herdr-agent-state.ts22
-rw-r--r--src/integration/assets/pi/herdr-agent-state.ts22
-rw-r--r--src/integration/mod.rs86
3 files changed, 120 insertions, 10 deletions
diff --git a/src/integration/assets/omp/herdr-agent-state.ts b/src/integration/assets/omp/herdr-agent-state.ts
index ba97b1c7..e0cfab5e 100644
--- a/src/integration/assets/omp/herdr-agent-state.ts
+++ b/src/integration/assets/omp/herdr-agent-state.ts
@@ -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=omp
-// HERDR_INTEGRATION_VERSION=3
+// HERDR_INTEGRATION_VERSION=4
// @ts-nocheck
import { createConnection } from "node:net";
@@ -157,6 +157,16 @@ function releaseAgent(): Promise<void> {
});
}
+function shouldReleaseOnSessionShutdown(event: any): boolean {
+ // OMP 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;
@@ -325,10 +335,12 @@ export default function (pi) {
publishState(true);
});
- pi.on("agent_start", () => {
+ pi.on("agent_start", (_event, ctx) => {
if (!rootSession) {
return;
}
+ updateSessionRef(ctx);
+ void reportSession();
clearPendingTimers();
clearFailureState();
agentActive = true;
@@ -357,11 +369,13 @@ export default function (pi) {
scheduleIdle();
});
- pi.on("session_shutdown", async () => {
+ pi.on("session_shutdown", async (event) => {
if (!rootSession) {
return;
}
clearPendingTimers();
- await releaseAgent();
+ if (shouldReleaseOnSessionShutdown(event)) {
+ await releaseAgent();
+ }
});
}
diff --git a/src/integration/assets/pi/herdr-agent-state.ts b/src/integration/assets/pi/herdr-agent-state.ts
index 6c9e1cae..4bee2763 100644
--- a/src/integration/assets/pi/herdr-agent-state.ts
+++ b/src/integration/assets/pi/herdr-agent-state.ts
@@ -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=3
+// HERDR_INTEGRATION_VERSION=4
// @ts-nocheck
import { createConnection } from "node:net";
@@ -157,6 +157,16 @@ function releaseAgent(): Promise<void> {
});
}
+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;
@@ -325,10 +335,12 @@ export default function (pi) {
publishState(true);
});
- pi.on("agent_start", () => {
+ pi.on("agent_start", (_event, ctx) => {
if (!rootSession) {
return;
}
+ updateSessionRef(ctx);
+ void reportSession();
clearPendingTimers();
clearFailureState();
agentActive = true;
@@ -357,11 +369,13 @@ export default function (pi) {
scheduleIdle();
});
- pi.on("session_shutdown", async () => {
+ pi.on("session_shutdown", async (event) => {
if (!rootSession) {
return;
}
clearPendingTimers();
- await releaseAgent();
+ if (shouldReleaseOnSessionShutdown(event)) {
+ await releaseAgent();
+ }
});
}
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index 06561e3d..287ab89b 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -12,10 +12,10 @@ pub(crate) const HERDR_TAB_ID_ENV_VAR: &str = "HERDR_TAB_ID";
pub(crate) const HERDR_WORKSPACE_ID_ENV_VAR: &str = "HERDR_WORKSPACE_ID";
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 = 3;
+const PI_INTEGRATION_VERSION: u32 = 4;
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 = 3;
+const OMP_INTEGRATION_VERSION: u32 = 4;
const PI_CODING_AGENT_DIR_ENV_VAR: &str = "PI_CODING_AGENT_DIR";
const CLAUDE_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
"herdr-agent-state.ps1"
@@ -6074,6 +6074,88 @@ mod tests {
}
#[test]
+ fn pi_extension_releases_only_for_quit_session_shutdown() {
+ let release_policy = PI_EXTENSION_ASSET
+ .find("function shouldReleaseOnSessionShutdown")
+ .expect("pi extension should centralize session shutdown release policy");
+ let quit_check = PI_EXTENSION_ASSET
+ .find("reason === \"quit\"")
+ .expect("pi extension should release only for true quit shutdowns");
+ let shutdown_handler = PI_EXTENSION_ASSET
+ .find("pi.on(\"session_shutdown\", async (event)")
+ .expect("pi extension should inspect the session_shutdown event");
+ let guarded_release = PI_EXTENSION_ASSET[shutdown_handler..]
+ .find("if (shouldReleaseOnSessionShutdown(event))")
+ .expect("pi extension should guard releaseAgent by shutdown reason");
+
+ assert!(release_policy < shutdown_handler);
+ assert!(release_policy < quit_check);
+ assert!(quit_check < shutdown_handler);
+ assert!(guarded_release > 0);
+ }
+
+ #[test]
+ fn pi_extension_refreshes_session_ref_before_agent_start_state() {
+ let agent_start = PI_EXTENSION_ASSET
+ .find("pi.on(\"agent_start\", (_event, ctx)")
+ .expect("pi extension should receive agent_start context");
+ let handler = &PI_EXTENSION_ASSET[agent_start..];
+ let update_session = handler
+ .find("updateSessionRef(ctx);")
+ .expect("pi extension should refresh the active session on agent_start");
+ let report_session = handler
+ .find("void reportSession();")
+ .expect("pi extension should report the refreshed session before state");
+ let publish_state = handler
+ .find("publishState();")
+ .expect("pi extension should publish working state after refreshing session");
+
+ assert!(update_session < report_session);
+ assert!(report_session < publish_state);
+ }
+
+ #[test]
+ fn omp_extension_releases_only_for_quit_session_shutdown() {
+ let release_policy = OMP_EXTENSION_ASSET
+ .find("function shouldReleaseOnSessionShutdown")
+ .expect("omp extension should centralize session shutdown release policy");
+ let quit_check = OMP_EXTENSION_ASSET
+ .find("reason === \"quit\"")
+ .expect("omp extension should release only for true quit shutdowns");
+ let shutdown_handler = OMP_EXTENSION_ASSET
+ .find("pi.on(\"session_shutdown\", async (event)")
+ .expect("omp extension should inspect the session_shutdown event");
+ let guarded_release = OMP_EXTENSION_ASSET[shutdown_handler..]
+ .find("if (shouldReleaseOnSessionShutdown(event))")
+ .expect("omp extension should guard releaseAgent by shutdown reason");
+
+ assert!(release_policy < shutdown_handler);
+ assert!(release_policy < quit_check);
+ assert!(quit_check < shutdown_handler);
+ assert!(guarded_release > 0);
+ }
+
+ #[test]
+ fn omp_extension_refreshes_session_ref_before_agent_start_state() {
+ let agent_start = OMP_EXTENSION_ASSET
+ .find("pi.on(\"agent_start\", (_event, ctx)")
+ .expect("omp extension should receive agent_start context");
+ let handler = &OMP_EXTENSION_ASSET[agent_start..];
+ let update_session = handler
+ .find("updateSessionRef(ctx);")
+ .expect("omp extension should refresh the active session on agent_start");
+ let report_session = handler
+ .find("void reportSession();")
+ .expect("omp extension should report the refreshed session before state");
+ let publish_state = handler
+ .find("publishState();")
+ .expect("omp extension should publish working state after refreshing session");
+
+ assert!(update_session < report_session);
+ assert!(report_session < publish_state);
+ }
+
+ #[test]
fn omp_session_hook_ignores_non_ui_sessions() {
let session_start_handler = OMP_EXTENSION_ASSET
.find("pi.on(\"session_start\"")