diff --git a/src/agent_resume.rs b/src/agent_resume.rs index 5012e991..dbd1d9f2 100644 --- a/src/agent_resume.rs +++ b/src/agent_resume.rs @@ -69,7 +69,7 @@ pub fn session_ref_from_report( agent_session_id.and_then(AgentSessionRef::id) } -pub fn normalize_claude_session_start_source(value: Option) -> Option { +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()), _ => None, @@ -501,32 +501,29 @@ mod tests { } #[test] - fn normalize_claude_session_start_source_allows_known_claude_values() { + fn normalize_session_start_source_allows_known_values() { assert_eq!( - normalize_claude_session_start_source(Some("startup".into())), + normalize_session_start_source(Some("startup".into())), Some("startup".into()) ); assert_eq!( - normalize_claude_session_start_source(Some("resume".into())), + normalize_session_start_source(Some("resume".into())), Some("resume".into()) ); assert_eq!( - normalize_claude_session_start_source(Some("clear".into())), + normalize_session_start_source(Some("clear".into())), Some("clear".into()) ); assert_eq!( - normalize_claude_session_start_source(Some("compact".into())), + normalize_session_start_source(Some("compact".into())), Some("compact".into()) ); assert_eq!( - normalize_claude_session_start_source(Some(" resume ".into())), + normalize_session_start_source(Some(" resume ".into())), Some("resume".into()) ); - assert_eq!( - normalize_claude_session_start_source(Some("other".into())), - None - ); - assert_eq!(normalize_claude_session_start_source(None), None); + assert_eq!(normalize_session_start_source(Some("other".into())), None); + assert_eq!(normalize_session_start_source(None), None); } #[test] diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs index ac8ddf4b..08a9b677 100644 --- a/src/app/api/panes.rs +++ b/src/app/api/panes.rs @@ -1239,7 +1239,7 @@ impl App { source: params.source, agent_label, seq: params.seq, - session_start_source: crate::agent_resume::normalize_claude_session_start_source( + session_start_source: crate::agent_resume::normalize_session_start_source( params.session_start_source, ), }); diff --git a/src/integration/assets/claude/herdr-agent-state.ps1 b/src/integration/assets/claude/herdr-agent-state.ps1 index dc78efd2..dcc9d4df 100644 --- a/src/integration/assets/claude/herdr-agent-state.ps1 +++ b/src/integration/assets/claude/herdr-agent-state.ps1 @@ -2,7 +2,7 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=claude -# HERDR_INTEGRATION_VERSION=6 +# HERDR_INTEGRATION_VERSION=7 param([string]$Action = "") @@ -17,6 +17,7 @@ try { exit 0 } +if (-not [string]::IsNullOrWhiteSpace($payload.agent_id)) { exit 0 } if ($payload.hook_event_name -eq "SubagentStop") { exit 0 } $sessionId = $payload.session_id diff --git a/src/integration/assets/claude/herdr-agent-state.sh b/src/integration/assets/claude/herdr-agent-state.sh index ce072189..cb5e840d 100644 --- a/src/integration/assets/claude/herdr-agent-state.sh +++ b/src/integration/assets/claude/herdr-agent-state.sh @@ -3,7 +3,7 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=claude -# HERDR_INTEGRATION_VERSION=6 +# HERDR_INTEGRATION_VERSION=7 set -eu @@ -50,6 +50,8 @@ if hook_input_file: hook_event_name = str(hook_input.get("hook_event_name") or "") is_subagent = bool(hook_input.get("agent_id")) +if is_subagent: + raise SystemExit(0) if hook_event_name == "SubagentStop": # SubagentStop is a completion event. Older Herdr integrations mapped it # to durable working, but Claude recap/away-summary can emit it after the diff --git a/src/integration/assets/codex/herdr-agent-state.ps1 b/src/integration/assets/codex/herdr-agent-state.ps1 index e56948f5..d837b78a 100644 --- a/src/integration/assets/codex/herdr-agent-state.ps1 +++ b/src/integration/assets/codex/herdr-agent-state.ps1 @@ -2,7 +2,7 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=codex -# HERDR_INTEGRATION_VERSION=5 +# HERDR_INTEGRATION_VERSION=6 param([string]$Action = "") @@ -17,11 +17,29 @@ try { exit 0 } +if ($payload.hook_event_name -and $payload.hook_event_name -ne "SessionStart") { exit 0 } + $sessionId = $payload.session_id if ([string]::IsNullOrWhiteSpace($sessionId)) { exit 0 } $seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() try { - & herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:codex --agent codex --seq $seq --agent-session-id $sessionId 2>$null | Out-Null + $args = @( + "pane", + "report-agent-session", + $env:HERDR_PANE_ID, + "--source", + "herdr:codex", + "--agent", + "codex", + "--seq", + "$seq", + "--agent-session-id", + "$sessionId" + ) + if ($payload.hook_event_name -eq "SessionStart" -and $payload.source -is [string] -and -not [string]::IsNullOrWhiteSpace($payload.source)) { + $args += @("--session-start-source", "$($payload.source)") + } + & herdr @args 2>$null | Out-Null } catch { } diff --git a/src/integration/assets/codex/herdr-agent-state.sh b/src/integration/assets/codex/herdr-agent-state.sh index 385c9a98..b3b08a78 100644 --- a/src/integration/assets/codex/herdr-agent-state.sh +++ b/src/integration/assets/codex/herdr-agent-state.sh @@ -3,7 +3,7 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=codex -# HERDR_INTEGRATION_VERSION=5 +# HERDR_INTEGRATION_VERSION=6 set -eu @@ -48,21 +48,31 @@ if hook_input_file: except Exception: hook_input = {} +hook_event_name = str(hook_input.get("hook_event_name") or "") +if hook_event_name and hook_event_name != "SessionStart": + raise SystemExit(0) + request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}" report_seq = time.time_ns() session_id = hook_input.get("session_id") agent_session_id = session_id if isinstance(session_id, str) and session_id else None +session_start_source = hook_input.get("source") if hook_event_name == "SessionStart" else None +if not isinstance(session_start_source, str) or not session_start_source: + session_start_source = None if agent_session_id: + params = { + "pane_id": pane_id, + "source": source, + "agent": "codex", + "seq": report_seq, + "agent_session_id": agent_session_id, + } + if session_start_source: + params["session_start_source"] = session_start_source request = { "id": request_id, "method": "pane.report_agent_session", - "params": { - "pane_id": pane_id, - "source": source, - "agent": "codex", - "seq": report_seq, - "agent_session_id": agent_session_id, - }, + "params": params, } else: raise SystemExit(0) diff --git a/src/integration/assets/hermes/__init__.py b/src/integration/assets/hermes/__init__.py index 6bb0fe8a..e6f6971d 100644 --- a/src/integration/assets/hermes/__init__.py +++ b/src/integration/assets/hermes/__init__.py @@ -1,7 +1,7 @@ """Hermes plugin installed by Herdr to report agent lifecycle state.""" # HERDR_INTEGRATION_ID=hermes -# HERDR_INTEGRATION_VERSION=2 +# HERDR_INTEGRATION_VERSION=3 from __future__ import annotations @@ -71,10 +71,6 @@ def _report(state: str, **kwargs) -> None: _send("pane.report_agent", params) -def _release() -> None: - _send("pane.release_agent", {}) - - def _working(**kwargs) -> None: _report("working", **kwargs) @@ -87,11 +83,6 @@ def _idle(**kwargs) -> None: _report("idle", **kwargs) -def _finalize(**kwargs) -> None: - del kwargs - _release() - - def register(ctx): ctx.register_hook("on_session_start", _idle) ctx.register_hook("pre_llm_call", _working) @@ -102,4 +93,3 @@ def register(ctx): ctx.register_hook("post_approval_response", _working) ctx.register_hook("post_llm_call", _idle) ctx.register_hook("on_session_end", _idle) - ctx.register_hook("on_session_finalize", _finalize) diff --git a/src/integration/assets/kilo/herdr-agent-state.js b/src/integration/assets/kilo/herdr-agent-state.js index aa1a0c1b..fc12919c 100644 --- a/src/integration/assets/kilo/herdr-agent-state.js +++ b/src/integration/assets/kilo/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=kilo -// HERDR_INTEGRATION_VERSION=1 +// HERDR_INTEGRATION_VERSION=2 import net from "node:net"; @@ -96,10 +96,6 @@ function reportState(state, sessionID) { return request("pane.report_agent", params); } -function releaseAgent() { - return request("pane.release_agent", {}); -} - export const HerdrAgentStatePlugin = async () => { if ( process.env.HERDR_ENV !== "1" || @@ -149,7 +145,6 @@ export const HerdrAgentStatePlugin = async () => { await reportState("idle", sessionID); break; case "session.deleted": - await releaseAgent(); break; default: break; diff --git a/src/integration/assets/kimi/herdr-agent-state.ps1 b/src/integration/assets/kimi/herdr-agent-state.ps1 index d9924acc..ff6bcefe 100644 --- a/src/integration/assets/kimi/herdr-agent-state.ps1 +++ b/src/integration/assets/kimi/herdr-agent-state.ps1 @@ -2,11 +2,11 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=kimi -# HERDR_INTEGRATION_VERSION=3 +# HERDR_INTEGRATION_VERSION=4 param([string]$Action = "") -if (@("session", "working", "blocked", "idle", "release") -notcontains $Action) { exit 0 } +if (@("session", "working", "blocked", "idle") -notcontains $Action) { exit 0 } if ($env:HERDR_ENV -ne "1") { exit 0 } if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 } @@ -21,9 +21,7 @@ $seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() $sessionId = if ($null -ne $payload -and -not [string]::IsNullOrWhiteSpace($payload.session_id)) { $payload.session_id } else { $null } try { - if ($Action -eq "release") { - & herdr pane release-agent $env:HERDR_PANE_ID --source herdr:kimi --agent kimi --seq $seq 2>$null | Out-Null - } elseif ($Action -eq "session") { + if ($Action -eq "session") { if ([string]::IsNullOrWhiteSpace($sessionId)) { exit 0 } & herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:kimi --agent kimi --agent-session-id $sessionId --seq $seq 2>$null | Out-Null } else { diff --git a/src/integration/assets/kimi/herdr-agent-state.sh b/src/integration/assets/kimi/herdr-agent-state.sh index 2c8d8567..b5b6cc4b 100644 --- a/src/integration/assets/kimi/herdr-agent-state.sh +++ b/src/integration/assets/kimi/herdr-agent-state.sh @@ -3,7 +3,7 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=kimi -# HERDR_INTEGRATION_VERSION=3 +# HERDR_INTEGRATION_VERSION=4 set -eu @@ -13,7 +13,7 @@ trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM cat >"$hook_input_file" 2>/dev/null || true case "$action" in - session|working|blocked|idle|release) ;; + session|working|blocked|idle) ;; *) exit 0 ;; esac @@ -54,18 +54,7 @@ agent_session_id = session_id if isinstance(session_id, str) and session_id else request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}" report_seq = time.time_ns() -if action == "release": - request = { - "id": request_id, - "method": "pane.release_agent", - "params": { - "pane_id": pane_id, - "source": source, - "agent": agent, - "seq": report_seq, - }, - } -elif action == "session": +if action == "session": if not agent_session_id: raise SystemExit(0) request = { diff --git a/src/integration/assets/omp/herdr-agent-state.ts b/src/integration/assets/omp/herdr-agent-state.ts index a6428cdd..a933e867 100644 --- a/src/integration/assets/omp/herdr-agent-state.ts +++ b/src/integration/assets/omp/herdr-agent-state.ts @@ -100,6 +100,35 @@ function parseDurationEnv(name: string, fallback: number): number { return parsed; } +function currentSessionRef(): Record | undefined { + if (currentAgentSessionPath) { + return { agent_session_path: currentAgentSessionPath }; + } + if (currentAgentSessionId) { + return { agent_session_id: currentAgentSessionId }; + } + return undefined; +} + +function reportSession(): Promise { + const sessionRef = currentSessionRef(); + if (!sessionRef) { + return Promise.resolve(); + } + + return sendRequest({ + id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent_session", + params: { + pane_id: paneId, + source, + agent: "omp", + seq: nextReportSeq(), + ...sessionRef, + }, + }); +} + function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise { return sendRequest({ id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, @@ -169,19 +198,6 @@ function retryableErrorMessage(event: any): string | undefined { return errorMessage || "retryable provider error"; } -function releaseAgent(): Promise { - return sendRequest({ - id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`, - method: "pane.release_agent", - params: { - pane_id: paneId, - source, - agent: "omp", - seq: nextReportSeq(), - }, - }); -} - export default function (pi) { if (!enabled()) { return; @@ -287,11 +303,12 @@ export default function (pi) { }); pi.on("session_start", (_event, ctx) => { - rootSession = ctx?.hasUI === true; - if (!rootSession) { + if (ctx?.hasUI !== true) { return; } + rootSession = true; updateSessionRef(ctx); + void reportSession(); publishState(true); }); @@ -326,12 +343,4 @@ export default function (pi) { scheduleIdle(); }); - - pi.on("session_shutdown", async () => { - if (!rootSession) { - return; - } - clearPendingTimers(); - await releaseAgent(); - }); } diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js index 1e9fff51..6836227b 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=5 +// HERDR_INTEGRATION_VERSION=6 import net from "node:net"; @@ -96,10 +96,6 @@ function reportState(state, sessionID) { return request("pane.report_agent", params); } -function releaseAgent() { - return request("pane.release_agent", {}); -} - export const HerdrAgentStatePlugin = async () => { if ( process.env.HERDR_ENV !== "1" || @@ -149,7 +145,6 @@ export const HerdrAgentStatePlugin = async () => { await reportState("idle", sessionID); break; case "session.deleted": - await releaseAgent(); break; default: break; diff --git a/src/integration/assets/pi/herdr-agent-state.ts b/src/integration/assets/pi/herdr-agent-state.ts index 0e620f1f..9af12cfd 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=2 +// HERDR_INTEGRATION_VERSION=3 // @ts-nocheck import { createConnection } from "node:net"; @@ -100,6 +100,35 @@ function withSessionRef(params: Record): Record | undefined { + if (currentAgentSessionPath) { + return { agent_session_path: currentAgentSessionPath }; + } + if (currentAgentSessionId) { + return { agent_session_id: currentAgentSessionId }; + } + return undefined; +} + +function reportSession(): Promise { + const sessionRef = currentSessionRef(); + if (!sessionRef) { + return Promise.resolve(); + } + + return sendRequest({ + id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent_session", + params: { + pane_id: paneId, + source, + agent: "pi", + seq: nextReportSeq(), + ...sessionRef, + }, + }); +} + function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise { return sendRequest({ id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, @@ -169,19 +198,6 @@ function retryableErrorMessage(event: any): string | undefined { return errorMessage || "retryable provider error"; } -function releaseAgent(): Promise { - 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(), - }, - }); -} - export default function (pi) { if (!enabled()) { return; @@ -197,6 +213,7 @@ export default function (pi) { let lastMessage: string | undefined; let idleTimer: ReturnType | undefined; let retryTimer: ReturnType | undefined; + let rootSession = false; function clearTimer(timer: ReturnType | undefined) { if (timer) { @@ -250,11 +267,6 @@ export default function (pi) { idleTimer.unref?.(); } - pi.on("session_start", (_event, ctx) => { - updateSessionRef(ctx); - publishState(true); - }); - function holdForRetry(message: string) { clearPendingTimers(); retryHoldActive = true; @@ -272,6 +284,9 @@ export default function (pi) { } pi.events.on("herdr:blocked", (data) => { + if (!rootSession) { + return; + } if (!data?.active) { blockedCount = Math.max(0, blockedCount - 1); if (blockedCount === 0) { @@ -287,7 +302,20 @@ export default function (pi) { publishState(); }); + pi.on("session_start", (_event, ctx) => { + if (ctx?.hasUI !== true) { + return; + } + rootSession = true; + updateSessionRef(ctx); + void reportSession(); + publishState(true); + }); + pi.on("agent_start", () => { + if (!rootSession) { + return; + } clearPendingTimers(); clearFailureState(); agentActive = true; @@ -295,6 +323,9 @@ export default function (pi) { }); pi.on("agent_end", (event) => { + if (!rootSession) { + 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 @@ -312,9 +343,4 @@ export default function (pi) { scheduleIdle(); }); - - pi.on("session_shutdown", async () => { - clearPendingTimers(); - await releaseAgent(); - }); } diff --git a/src/integration/mod.rs b/src/integration/mod.rs index 59eaebd9..0a00240b 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -12,7 +12,7 @@ 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 = 2; +const PI_INTEGRATION_VERSION: u32 = 3; 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; @@ -27,7 +27,7 @@ const CLAUDE_HOOK_ASSET: &str = if cfg!(windows) { } else { include_str!("assets/claude/herdr-agent-state.sh") }; -const CLAUDE_INTEGRATION_VERSION: u32 = 6; +const CLAUDE_INTEGRATION_VERSION: u32 = 7; const CLAUDE_CONFIG_DIR_ENV_VAR: &str = "CLAUDE_CONFIG_DIR"; const CODEX_HOOK_INSTALL_NAME: &str = if cfg!(windows) { "herdr-agent-state.ps1" @@ -39,7 +39,7 @@ const CODEX_HOOK_ASSET: &str = if cfg!(windows) { } else { include_str!("assets/codex/herdr-agent-state.sh") }; -const CODEX_INTEGRATION_VERSION: u32 = 5; +const CODEX_INTEGRATION_VERSION: u32 = 6; const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME"; const KIMI_HOOK_INSTALL_NAME: &str = if cfg!(windows) { "herdr-agent-state.ps1" @@ -51,12 +51,12 @@ const KIMI_HOOK_ASSET: &str = if cfg!(windows) { } else { include_str!("assets/kimi/herdr-agent-state.sh") }; -const KIMI_INTEGRATION_VERSION: u32 = 3; +const KIMI_INTEGRATION_VERSION: u32 = 4; const KIMI_CODE_HOME_ENV_VAR: &str = "KIMI_CODE_HOME"; const KIMI_CONFIG_BLOCK_BEGIN: &str = "# >>> herdr kimi integration"; const KIMI_CONFIG_BLOCK_END: &str = "# <<< herdr kimi integration"; const KIMI_MIN_VERSION: &str = "0.14.0"; -const KIMI_HOOK_EVENTS: [(&str, &str); 10] = [ +const KIMI_HOOK_EVENTS: [(&str, &str); 9] = [ ("SessionStart", "session"), ("UserPromptSubmit", "working"), ("PreToolUse", "working"), @@ -66,7 +66,6 @@ const KIMI_HOOK_EVENTS: [(&str, &str); 10] = [ ("PermissionResult", "working"), ("Stop", "idle"), ("Interrupt", "idle"), - ("SessionEnd", "release"), ]; const COPILOT_HOOK_INSTALL_NAME: &str = if cfg!(windows) { "herdr-agent-state.ps1" @@ -136,16 +135,16 @@ 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 = 5; +const OPENCODE_INTEGRATION_VERSION: u32 = 6; 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 = 1; +const KILO_INTEGRATION_VERSION: u32 = 2; const HERMES_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state"; const HERMES_PLUGIN_MANIFEST_INSTALL_NAME: &str = "plugin.yaml"; const HERMES_PLUGIN_INIT_INSTALL_NAME: &str = "__init__.py"; const HERMES_PLUGIN_MANIFEST_ASSET: &str = include_str!("assets/hermes/plugin.yaml"); const HERMES_PLUGIN_INIT_ASSET: &str = include_str!("assets/hermes/__init__.py"); -const HERMES_INTEGRATION_VERSION: u32 = 2; +const HERMES_INTEGRATION_VERSION: u32 = 3; const QODERCLI_HOOK_INSTALL_NAME: &str = if cfg!(windows) { "herdr-agent-state.ps1" } else { @@ -4434,7 +4433,7 @@ mod tests { assert_eq!(claude.path, hook_path); assert_eq!(claude.installed_version, Some(1)); - assert_eq!(claude.expected_version, 6); + assert_eq!(claude.expected_version, 7); assert_eq!(claude.state, IntegrationStatusKind::Outdated); std::env::remove_var("HOME"); @@ -4464,7 +4463,7 @@ mod tests { assert_eq!(claude.path, hook_path); assert_eq!(claude.installed_version, Some(2)); - assert_eq!(claude.expected_version, 6); + assert_eq!(claude.expected_version, 7); assert_eq!(claude.state, IntegrationStatusKind::Outdated); std::env::remove_var("HOME"); @@ -4597,7 +4596,7 @@ mod tests { assert_eq!(codex.path, hook_path); assert_eq!(codex.installed_version, Some(2)); - assert_eq!(codex.expected_version, 5); + assert_eq!(codex.expected_version, 6); assert_eq!(codex.state, IntegrationStatusKind::Outdated); std::env::remove_var("HOME"); @@ -5966,28 +5965,66 @@ mod tests { #[test] fn bundled_integration_assets_report_session_refs() { - assert!(PI_EXTENSION_ASSET.contains("agent_session_path: currentAgentSessionPath")); - assert!(PI_EXTENSION_ASSET.contains("agent_session_id: currentAgentSessionId")); - assert!(PI_EXTENSION_ASSET.contains("publishState(true)")); - assert!(OMP_EXTENSION_ASSET.contains("agent_session_path: currentAgentSessionPath")); - assert!(OMP_EXTENSION_ASSET.contains("agent_session_id: currentAgentSessionId")); - assert!(OMP_EXTENSION_ASSET.contains("publishState(true)")); - assert!(CLAUDE_HOOK_ASSET.contains("agent_session_id")); - assert!(CLAUDE_HOOK_ASSET.contains("agent_session_path")); - assert!(CLAUDE_HOOK_ASSET.contains("session_start_source")); - assert!(CLAUDE_HOOK_ASSET.contains("pane.report_agent_session")); + assert!(PI_EXTENSION_ASSET.contains("agent_session_path")); + assert!(PI_EXTENSION_ASSET.contains("agent_session_id")); + assert!(PI_EXTENSION_ASSET.contains("ctx?.hasUI !== true")); + assert!(PI_EXTENSION_ASSET.contains("pane.report_agent_session")); + assert!(PI_EXTENSION_ASSET.contains("pane.report_agent\"")); + assert!(PI_EXTENSION_ASSET.contains("pi.on(\"agent_start\"")); + assert!(PI_EXTENSION_ASSET.contains("pi.on(\"agent_end\"")); + assert!(!PI_EXTENSION_ASSET.contains("pane.release_agent")); + assert!(!PI_EXTENSION_ASSET.contains("session_shutdown")); + assert!(OMP_EXTENSION_ASSET.contains("agent_session_path")); + assert!(OMP_EXTENSION_ASSET.contains("agent_session_id")); + assert!(OMP_EXTENSION_ASSET.contains("ctx?.hasUI !== true")); + assert!(OMP_EXTENSION_ASSET.contains("pane.report_agent_session")); + assert!(OMP_EXTENSION_ASSET.contains("pane.report_agent\"")); + assert!(OMP_EXTENSION_ASSET.contains("pi.on(\"agent_start\"")); + assert!(OMP_EXTENSION_ASSET.contains("pi.on(\"agent_end\"")); + assert!(!OMP_EXTENSION_ASSET.contains("pane.release_agent")); + assert!(!OMP_EXTENSION_ASSET.contains("session_shutdown")); + assert!( + CLAUDE_HOOK_ASSET.contains("agent_session_id") + || CLAUDE_HOOK_ASSET.contains("--agent-session-id") + ); + assert!( + CLAUDE_HOOK_ASSET.contains("agent_session_path") + || CLAUDE_HOOK_ASSET.contains("--agent-session-path") + ); + assert!(CLAUDE_HOOK_ASSET.contains("agent_id")); + assert!( + CLAUDE_HOOK_ASSET.contains("session_start_source") + || CLAUDE_HOOK_ASSET.contains("--session-start-source") + ); + assert!( + CLAUDE_HOOK_ASSET.contains("pane.report_agent_session") + || CLAUDE_HOOK_ASSET.contains("report-agent-session") + ); assert!(!CLAUDE_HOOK_ASSET.contains("\"state\": action")); assert!(!CLAUDE_HOOK_ASSET.contains("pane.release_agent")); - assert!(CODEX_HOOK_ASSET.contains("HERDR_HOOK_INPUT_FILE")); - assert!(CODEX_HOOK_ASSET.contains("agent_session_id")); - assert!(CODEX_HOOK_ASSET.contains("pane.report_agent_session")); + assert!( + CODEX_HOOK_ASSET.contains("HERDR_HOOK_INPUT_FILE") + || CODEX_HOOK_ASSET.contains("In.ReadToEnd") + ); + assert!( + CODEX_HOOK_ASSET.contains("agent_session_id") + || CODEX_HOOK_ASSET.contains("--agent-session-id") + ); + assert!( + CODEX_HOOK_ASSET.contains("session_start_source") + || CODEX_HOOK_ASSET.contains("--session-start-source") + ); + assert!( + CODEX_HOOK_ASSET.contains("pane.report_agent_session") + || CODEX_HOOK_ASSET.contains("report-agent-session") + ); assert!(!CODEX_HOOK_ASSET.contains("\"state\": action")); assert!(!CODEX_HOOK_ASSET.contains("pane.release_agent")); assert!(KIMI_HOOK_ASSET.contains("source = \"herdr:kimi\"")); assert!(KIMI_HOOK_ASSET.contains("agent_session_id")); assert!(KIMI_HOOK_ASSET.contains("pane.report_agent_session")); assert!(KIMI_HOOK_ASSET.contains("\"state\": action")); - assert!(KIMI_HOOK_ASSET.contains("pane.release_agent")); + assert!(!KIMI_HOOK_ASSET.contains("pane.release_agent")); assert!(COPILOT_HOOK_ASSET.contains("agent_session_id")); assert!(COPILOT_HOOK_ASSET.contains("pane.report_agent_session")); assert!(!COPILOT_HOOK_ASSET.contains("\"state\":")); @@ -6006,16 +6043,18 @@ mod tests { assert!(OPENCODE_PLUGIN_ASSET.contains("params.agent_session_id = sessionID")); assert!(OPENCODE_PLUGIN_ASSET.contains("pane.report_agent_session")); assert!(OPENCODE_PLUGIN_ASSET.contains("reportState")); - assert!(OPENCODE_PLUGIN_ASSET.contains("pane.release_agent")); + assert!(!OPENCODE_PLUGIN_ASSET.contains("pane.release_agent")); assert!(KILO_PLUGIN_ASSET.contains("SOURCE = \"herdr:kilo\"")); assert!(KILO_PLUGIN_ASSET.contains("AGENT = \"kilo\"")); assert!(KILO_PLUGIN_ASSET.contains("pane.report_agent_session")); assert!(KILO_PLUGIN_ASSET.contains("reportState")); - assert!(KILO_PLUGIN_ASSET.contains("pane.release_agent")); + assert!(!KILO_PLUGIN_ASSET.contains("pane.release_agent")); assert!(HERMES_PLUGIN_INIT_ASSET.contains("session_id = _session_id(kwargs)")); assert!(HERMES_PLUGIN_INIT_ASSET.contains("agent_session_id")); assert!(HERMES_PLUGIN_INIT_ASSET.contains("pane.report_agent\",")); - assert!(HERMES_PLUGIN_INIT_ASSET.contains("pane.release_agent")); + assert!(HERMES_PLUGIN_INIT_ASSET.contains("on_session_end")); + assert!(!HERMES_PLUGIN_INIT_ASSET.contains("on_session_finalize")); + assert!(!HERMES_PLUGIN_INIT_ASSET.contains("pane.release_agent")); assert!(QODERCLI_HOOK_ASSET.contains("HERDR_HOOK_INPUT_FILE")); assert!(QODERCLI_HOOK_ASSET.contains("agent_session_id")); assert!(QODERCLI_HOOK_ASSET.contains("pane.report_agent_session")); @@ -6035,26 +6074,19 @@ mod tests { } #[test] - fn omp_root_session_guard_is_instance_scoped() { - let export_start = OMP_EXTENSION_ASSET - .find("export default function (pi)") - .expect("omp extension exports a function"); - let root_session_decl = OMP_EXTENSION_ASSET - .find("let rootSession = false") - .expect("omp extension declares root session guard"); + fn omp_session_hook_ignores_non_ui_sessions() { let session_start_handler = OMP_EXTENSION_ASSET .find("pi.on(\"session_start\"") .expect("omp extension registers session_start handler"); + let non_ui_guard = OMP_EXTENSION_ASSET + .find("ctx?.hasUI !== true") + .expect("omp extension checks UI context"); + let session_report = OMP_EXTENSION_ASSET + .find("void reportSession()") + .expect("omp extension reports root session"); - assert_eq!( - OMP_EXTENSION_ASSET - .matches("let rootSession = false") - .count(), - 1 - ); - assert!(OMP_EXTENSION_ASSET.contains("rootSession = ctx?.hasUI === true")); - assert!(export_start < root_session_decl); - assert!(root_session_decl < session_start_handler); + assert!(session_start_handler < non_ui_guard); + assert!(non_ui_guard < session_report); } #[test] diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 01b6bdfe..b5a05168 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -264,6 +264,14 @@ impl TerminalState { } self.hook_authority = None; } + if process_exited + && self + .persisted_agent_session + .as_ref() + .is_some_and(|session| crate::detect::parse_agent_label(&session.agent) == agent) + { + self.persisted_agent_session = None; + } if self.hook_authority_not_newer_than(now) && (self.hook_authority_conflicts_with_detected_agent(agent) || (previous_detected_agent.is_some() @@ -273,19 +281,20 @@ impl TerminalState { == previous_detected_agent }))) { + let durable_session = self.hook_authority.as_ref().and_then(|authority| { + authority.session_ref.as_ref().map(|session_ref| { + crate::agent_resume::PersistedAgentSession { + source: authority.source.clone(), + agent: authority.agent_label.clone(), + session_ref: session_ref.clone(), + } + }) + }); self.suppress_current_full_lifecycle_hook_authority( FullLifecycleHookSuppressionReason::HookClear, ); self.hook_authority = None; - } - let detected_agent_changed_or_disappeared = - previous_detected_agent.is_some() && agent != previous_detected_agent; - let persisted_agent_was_previously_detected = - self.persisted_agent_session_belongs_to_detected_agent(previous_detected_agent); - if self.persisted_agent_session_conflicts_with_detected_agent(agent) - || detected_agent_changed_or_disappeared && persisted_agent_was_previously_detected - { - self.persisted_agent_session = None; + self.persisted_agent_session = durable_session; } TerminalStateMutation { effective_state_change: self.recompute_effective_state( @@ -388,11 +397,13 @@ impl TerminalState { &agent_label, &session_ref, ); - if self.known_agent_label_conflicts_with_detected_agent(&agent_label) { + if self.known_agent_label_conflicts_with_detected_agent(&agent_label) + || self.current_session_owner_conflicts(&source, &agent_label) + { return None; } let session_ref = session_ref.map(|session_ref| { - self.conflicting_current_session_ref(&source, &agent_label, &session_ref, None) + self.conflicting_same_owner_session_ref(&source, &agent_label, &session_ref, None) .unwrap_or(session_ref) }); if self.live_full_lifecycle_hook_authority_conflicts_with_session( @@ -481,32 +492,6 @@ impl TerminalState { && !self.hook_authority_conflicts_with_detected_agent(detected_agent) } - fn persisted_agent_session_conflicts_with_detected_agent( - &self, - detected_agent: Option, - ) -> bool { - let Some(detected_agent) = detected_agent else { - return false; - }; - self.persisted_agent_session - .as_ref() - .and_then(|session| crate::detect::parse_agent_label(&session.agent)) - .is_some_and(|agent| agent != detected_agent) - } - - fn persisted_agent_session_belongs_to_detected_agent( - &self, - detected_agent: Option, - ) -> bool { - let Some(detected_agent) = detected_agent else { - return false; - }; - self.persisted_agent_session - .as_ref() - .and_then(|session| crate::detect::parse_agent_label(&session.agent)) - .is_some_and(|agent| agent == detected_agent) - } - fn persisted_agent_session_matches(&self, source: &str, agent: &str) -> bool { self.persisted_agent_session .as_ref() @@ -802,7 +787,15 @@ impl TerminalState { }) } - fn conflicting_current_session_ref( + fn current_session_owner_conflicts(&self, source: &str, agent_label: &str) -> bool { + self.current_session_identity_for_persistence().is_some_and( + |(current_source, current_agent, _, _)| { + current_source != source || current_agent != agent_label + }, + ) + } + + fn conflicting_same_owner_session_ref( &self, source: &str, agent_label: &str, @@ -815,7 +808,7 @@ impl TerminalState { && current_agent == agent_label && current_kind == crate::agent_resume::AgentSessionRefKind::Id && session_ref.kind == crate::agent_resume::AgentSessionRefKind::Id - && (current_kind != session_ref.kind || current_value != session_ref.value) + && current_value != session_ref.value && !Self::session_start_source_allows_session_replacement( source, agent_label, @@ -834,9 +827,18 @@ impl TerminalState { agent_label: &str, session_start_source: Option<&str>, ) -> bool { - source == "herdr:claude" - && agent_label == "claude" - && matches!(session_start_source, Some("clear" | "resume" | "compact")) + matches!( + (source, agent_label, session_start_source), + ( + "herdr:claude", + "claude", + Some("clear" | "resume" | "compact") + ) | ( + "herdr:codex", + "codex", + Some("startup" | "clear" | "resume" | "compact") + ) + ) } pub fn set_persisted_agent_session( @@ -871,14 +873,15 @@ impl TerminalState { if self.known_agent_label_conflicts_with_detected_agent(&agent_label) { return None; } - if self - .conflicting_current_session_ref( - &source, - &agent_label, - &session_ref, - session_start_source.as_deref(), - ) - .is_some() + if self.current_session_owner_conflicts(&source, &agent_label) + || self + .conflicting_same_owner_session_ref( + &source, + &agent_label, + &session_ref, + session_start_source.as_deref(), + ) + .is_some() { return None; } @@ -1009,6 +1012,10 @@ impl TerminalState { if !matches_current_agent && !matches_persisted_session { return None; } + let preserve_foreign_persisted_session = self + .persisted_agent_session + .as_ref() + .is_some_and(|session| session.source != source || session.agent != agent_label); let now = Instant::now(); let previous_agent_label = self.effective_agent_label().map(str::to_string); @@ -1026,7 +1033,10 @@ impl TerminalState { self.fallback_visible_blocker = false; self.fallback_observed_at = None; self.hook_authority = None; - self.persisted_agent_session = None; + if !preserve_foreign_persisted_session { + self.persisted_agent_session = None; + } + let current_session = self.current_session_identity_for_persistence(); Some(TerminalStateMutation { effective_state_change: self.recompute_effective_state( previous_agent_label, @@ -1035,7 +1045,7 @@ impl TerminalState { previous_presentation, now, ), - session_ref_changed: previous_session.is_some(), + session_ref_changed: previous_session != current_session, }) } @@ -3097,22 +3107,23 @@ mod tests { } #[test] - fn accepted_hook_report_marks_changed_when_session_identity_changes() { + fn accepted_hook_report_marks_changed_when_same_owner_session_identity_changes() { let mut terminal = test_terminal(); terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession { - source: "herdr:opencode".into(), - agent: "opencode".into(), - session_ref: crate::agent_resume::AgentSessionRef::id("same-session").unwrap(), + source: "herdr:pi".into(), + agent: "pi".into(), + session_ref: crate::agent_resume::AgentSessionRef::path(test_session_path("old.jsonl")) + .unwrap(), }); let mutation = terminal .set_hook_authority_with_session_ref( - "herdr:hermes".into(), - "hermes".into(), + "herdr:pi".into(), + "pi".into(), AgentState::Working, None, None, - crate::agent_resume::AgentSessionRef::id("same-session"), + crate::agent_resume::AgentSessionRef::path(test_session_path("new.jsonl")), Some(20), ) .expect("accepted report"); @@ -3222,6 +3233,106 @@ mod tests { } } + #[test] + fn codex_lifecycle_session_ref_replaces_existing_session_ref() { + for session_start_source in ["startup", "clear", "resume", "compact"] { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:codex".into(), + "codex".into(), + crate::agent_resume::AgentSessionRef::id("codex-session"), + Some(20), + ) + .expect("initial session should be accepted"); + + let next_session = format!("codex-{session_start_source}-session"); + let mutation = terminal + .set_agent_session_ref_for_session_start( + "herdr:codex".into(), + "codex".into(), + crate::agent_resume::AgentSessionRef::id(&next_session), + Some(21), + Some(session_start_source.into()), + ) + .unwrap_or_else(|| panic!("{session_start_source} 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(next_session.as_str()) + ); + } + } + + #[test] + fn different_owner_session_ref_does_not_replace_existing_session_ref() { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:droid".into(), + "droid".into(), + crate::agent_resume::AgentSessionRef::id("droid-session"), + Some(20), + ) + .expect("initial session should be accepted"); + + let mutation = terminal.set_agent_session_ref_for_session_start( + "herdr:claude".into(), + "claude".into(), + crate::agent_resume::AgentSessionRef::id("claude-session"), + Some(21), + Some("resume".into()), + ); + + assert!(mutation.is_none()); + assert_eq!( + terminal.persisted_agent_session.as_ref().map(|session| ( + session.source.as_str(), + session.agent.as_str(), + session.session_ref.value.as_str() + )), + Some(("herdr:droid", "droid", "droid-session")) + ); + } + + #[test] + fn different_owner_full_lifecycle_hook_does_not_replace_existing_session_ref() { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:droid".into(), + "droid".into(), + crate::agent_resume::AgentSessionRef::id("droid-session"), + Some(20), + ) + .expect("initial session should be accepted"); + + let mutation = terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Working, + None, + None, + crate::agent_resume::AgentSessionRef::path("/tmp/pi-session.jsonl"), + Some(21), + ); + + assert!(mutation.is_none()); + assert!(terminal.hook_authority.is_none()); + assert_eq!( + terminal.persisted_agent_session.as_ref().map(|session| ( + session.source.as_str(), + session.agent.as_str(), + session.session_ref.value.as_str() + )), + Some(("herdr:droid", "droid", "droid-session")) + ); + } + #[test] fn repeated_same_agent_session_ref_is_accepted_without_session_change() { let mut terminal = test_terminal(); @@ -3286,7 +3397,7 @@ mod tests { } #[test] - fn different_same_agent_session_ref_is_accepted_after_detection_clears_current_session() { + fn detected_agent_clear_does_not_clear_current_session_ref() { let mut terminal = test_terminal(); terminal.set_detected_state(Some(Agent::Claude), AgentState::Working); terminal @@ -3299,24 +3410,22 @@ mod tests { .expect("initial session should be accepted"); let clear = terminal.set_detected_state_with_mutation(None, AgentState::Unknown); - assert!(clear.session_ref_changed); + assert!(!clear.session_ref_changed); - let mutation = terminal - .set_agent_session_ref( - "herdr:claude".into(), - "claude".into(), - crate::agent_resume::AgentSessionRef::id("new-session"), - Some(21), - ) - .expect("new session should be accepted after clear"); + let mutation = terminal.set_agent_session_ref( + "herdr:claude".into(), + "claude".into(), + crate::agent_resume::AgentSessionRef::id("new-session"), + Some(21), + ); - assert!(mutation.session_ref_changed); + assert!(mutation.is_none()); assert_eq!( terminal .persisted_agent_session .as_ref() .map(|session| session.session_ref.value.as_str()), - Some("new-session") + Some("claude-session") ); } @@ -3382,6 +3491,86 @@ mod tests { assert!(terminal.persisted_agent_session.is_none()); } + #[test] + fn release_agent_preserves_foreign_persisted_session_ref() { + let mut terminal = test_terminal(); + terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession { + source: "herdr:claude".into(), + agent: "claude".into(), + session_ref: crate::agent_resume::AgentSessionRef::id("claude-session").unwrap(), + }); + terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle); + + let mutation = terminal + .release_agent_with_mutation("herdr:pi", "pi", Some(21)) + .expect("visible agent release should be accepted"); + + assert!(!mutation.session_ref_changed); + assert_eq!( + terminal.persisted_agent_session.as_ref().map(|session| ( + session.source.as_str(), + session.agent.as_str(), + session.session_ref.value.as_str() + )), + Some(("herdr:claude", "claude", "claude-session")) + ); + } + + #[test] + fn process_exit_clears_matching_persisted_session_ref() { + let mut terminal = test_terminal(); + terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession { + source: "herdr:pi".into(), + agent: "pi".into(), + session_ref: crate::agent_resume::AgentSessionRef::path(test_session_path("pi.jsonl")) + .unwrap(), + }); + terminal.set_detected_state(Some(Agent::Pi), AgentState::Working); + + let mutation = terminal.set_detected_state_with_screen_signals_at( + Some(Agent::Pi), + AgentState::Idle, + false, + false, + false, + true, + std::time::Instant::now(), + ); + + assert!(mutation.session_ref_changed); + assert!(terminal.persisted_agent_session.is_none()); + } + + #[test] + fn process_exit_preserves_foreign_persisted_session_ref() { + let mut terminal = test_terminal(); + terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession { + source: "herdr:claude".into(), + agent: "claude".into(), + session_ref: crate::agent_resume::AgentSessionRef::id("claude-session").unwrap(), + }); + terminal.set_detected_state(Some(Agent::Pi), AgentState::Working); + + let mutation = terminal.set_detected_state_with_screen_signals_at( + Some(Agent::Pi), + AgentState::Idle, + false, + false, + false, + true, + std::time::Instant::now(), + ); + + assert!(!mutation.session_ref_changed); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| session.session_ref.value.as_str()), + Some("claude-session") + ); + } + #[test] fn respawn_cleanup_resets_restored_agent_status() { let mut terminal = test_terminal(); @@ -3404,7 +3593,7 @@ mod tests { } #[test] - fn detected_conflict_clears_session_ref() { + fn detected_conflict_clears_live_hook_but_preserves_session_ref() { let mut terminal = test_terminal(); terminal.set_hook_authority_with_session_ref( "herdr:claude".into(), @@ -3419,8 +3608,16 @@ mod tests { let mutation = terminal.set_detected_state_with_mutation(Some(Agent::Grok), AgentState::Idle); - assert!(mutation.session_ref_changed); + assert!(!mutation.session_ref_changed); assert!(terminal.hook_authority.is_none()); + assert_eq!( + terminal.persisted_agent_session.as_ref().map(|session| ( + session.source.as_str(), + session.agent.as_str(), + session.session_ref.value.as_str() + )), + Some(("herdr:claude", "claude", "claude-session")) + ); } #[test] @@ -3446,7 +3643,7 @@ mod tests { } #[test] - fn detected_agent_disappearance_clears_matching_persisted_session_ref() { + fn detected_agent_disappearance_preserves_matching_persisted_session_ref() { let mut terminal = test_terminal(); terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession { source: "herdr:opencode".into(), @@ -3460,8 +3657,8 @@ mod tests { assert!(terminal.persisted_agent_session.is_some()); let second = terminal.set_detected_state_with_mutation(None, AgentState::Unknown); - assert!(second.session_ref_changed); - assert!(terminal.persisted_agent_session.is_none()); + assert!(!second.session_ref_changed); + assert!(terminal.persisted_agent_session.is_some()); } #[test] diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index 1870f670..f23b4d3a 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -1345,7 +1345,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 (v2)")); + assert!(status_stdout.contains("pi: current (v3)")); assert!(status_stdout.contains("claude: not installed")); let integration_uninstall = Command::new(env!("CARGO_BIN_EXE_herdr"))