From 74fa90cecc6f6dfd93dc64893659771001ad2d82 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Wed, 24 Jun 2026 00:52:07 +0300 Subject: [PATCH] fix: scope opencode hook to main agent and adopt new sessions refs #765 --- src/agent_resume.rs | 8 ++- .../assets/opencode/herdr-agent-state.js | 31 ++++++++- src/integration/mod.rs | 2 +- src/terminal/state.rs | 66 ++++++++++++++++++- 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/agent_resume.rs b/src/agent_resume.rs index dbd1d9f2..f23ff5e0 100644 --- a/src/agent_resume.rs +++ b/src/agent_resume.rs @@ -71,7 +71,9 @@ pub fn session_ref_from_report( pub fn normalize_session_start_source(value: Option) -> Option { match value.as_deref().map(str::trim) { - Some(source @ ("startup" | "resume" | "clear" | "compact")) => Some(source.to_string()), + Some(source @ ("startup" | "resume" | "clear" | "compact" | "new")) => { + Some(source.to_string()) + } _ => None, } } @@ -518,6 +520,10 @@ mod tests { normalize_session_start_source(Some("compact".into())), Some("compact".into()) ); + assert_eq!( + normalize_session_start_source(Some("new".into())), + Some("new".into()) + ); assert_eq!( normalize_session_start_source(Some(" resume ".into())), Some("resume".into()) diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js index 6836227b..a46d1d88 100644 --- a/src/integration/assets/opencode/herdr-agent-state.js +++ b/src/integration/assets/opencode/herdr-agent-state.js @@ -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=opencode -// HERDR_INTEGRATION_VERSION=6 +// HERDR_INTEGRATION_VERSION=7 import net from "node:net"; @@ -10,6 +10,11 @@ const SOURCE = "herdr:opencode"; const AGENT = "opencode"; let reportSeq = Date.now() * 1000; +// Subagent (task tool) sessions carry a parentID; the main agent session does +// not. Their lifecycle events would otherwise clobber the pane's real state, so +// learn child session ids from session.created/updated and drop their reports. +const childSessions = new Set(); + function nextReportSeq() { reportSeq += 1; return reportSeq; @@ -81,11 +86,15 @@ function request(method, params) { }); } -function reportSession(sessionID) { +function reportSession(sessionID, sessionStartSource) { if (!sessionID) { return Promise.resolve(); } - return request("pane.report_agent_session", { agent_session_id: sessionID }); + const params = { agent_session_id: sessionID }; + if (sessionStartSource) { + params.session_start_source = sessionStartSource; + } + return request("pane.report_agent_session", params); } function reportState(state, sessionID) { @@ -107,6 +116,9 @@ export const HerdrAgentStatePlugin = async () => { return { "chat.message": async ({ sessionID }) => { + if (sessionID && childSessions.has(sessionID)) { + return; + } await reportState("working", sessionID); }, event: async ({ event }) => { @@ -114,8 +126,21 @@ export const HerdrAgentStatePlugin = async () => { const properties = event?.properties ?? {}; const sessionID = sessionIDFromProperties(properties); + const info = properties.info; + if (info?.id && info.parentID) { + childSessions.add(info.id); + } + if (sessionID && childSessions.has(sessionID)) { + return; + } + switch (type) { case "session.created": + // A root session.created is a genuine new-session start (subagent + // creates are dropped above). Signal it so herdr replaces the pane's + // prior session id instead of treating the change as cross-talk. + await reportSession(sessionID, "new"); + break; case "session.updated": await reportSession(sessionID); break; diff --git a/src/integration/mod.rs b/src/integration/mod.rs index d08ad32e..06561e3d 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -135,7 +135,7 @@ const DROID_REMOVED_LIFECYCLE_HOOK_EVENTS: [(&str, &str); 9] = [ ]; const OPENCODE_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state.js"; const OPENCODE_PLUGIN_ASSET: &str = include_str!("assets/opencode/herdr-agent-state.js"); -const OPENCODE_INTEGRATION_VERSION: u32 = 6; +const OPENCODE_INTEGRATION_VERSION: u32 = 7; const KILO_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state.js"; const KILO_PLUGIN_ASSET: &str = include_str!("assets/kilo/herdr-agent-state.js"); const KILO_INTEGRATION_VERSION: u32 = 2; diff --git a/src/terminal/state.rs b/src/terminal/state.rs index b5a05168..e7923719 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -837,7 +837,7 @@ impl TerminalState { "herdr:codex", "codex", Some("startup" | "clear" | "resume" | "compact") - ) + ) | ("herdr:opencode", "opencode", Some("new")) ) } @@ -3268,6 +3268,70 @@ mod tests { } } + #[test] + fn opencode_new_session_ref_replaces_existing_session_ref() { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:opencode".into(), + "opencode".into(), + crate::agent_resume::AgentSessionRef::id("opencode-old"), + Some(20), + ) + .expect("initial session should be accepted"); + + let mutation = terminal + .set_agent_session_ref_for_session_start( + "herdr:opencode".into(), + "opencode".into(), + crate::agent_resume::AgentSessionRef::id("opencode-new"), + Some(21), + Some("new".into()), + ) + .expect("new should replace the session"); + + assert!(mutation.session_ref_changed); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| session.session_ref.value.as_str()), + Some("opencode-new") + ); + } + + #[test] + fn opencode_session_ref_without_start_source_does_not_replace_existing() { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:opencode".into(), + "opencode".into(), + crate::agent_resume::AgentSessionRef::id("opencode-old"), + Some(20), + ) + .expect("initial session should be accepted"); + + // session.updated reports carry no session_start_source, so a different + // id must not displace the established session (cross-talk guard). + let mutation = terminal.set_agent_session_ref_for_session_start( + "herdr:opencode".into(), + "opencode".into(), + crate::agent_resume::AgentSessionRef::id("opencode-other"), + Some(21), + None, + ); + + assert!(mutation.is_none()); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| session.session_ref.value.as_str()), + Some("opencode-old") + ); + } + #[test] fn different_owner_session_ref_does_not_replace_existing_session_ref() { let mut terminal = test_terminal();