parent
73b137a4ae
commit
92a10fc044
|
|
@ -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<String>) -> Option<String> {
|
||||
pub fn normalize_session_start_source(value: Option<String>) -> Option<String> {
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,35 @@ function parseDurationEnv(name: string, fallback: number): number {
|
|||
return parsed;
|
||||
}
|
||||
|
||||
function currentSessionRef(): Record<string, unknown> | undefined {
|
||||
if (currentAgentSessionPath) {
|
||||
return { agent_session_path: currentAgentSessionPath };
|
||||
}
|
||||
if (currentAgentSessionId) {
|
||||
return { agent_session_id: currentAgentSessionId };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function reportSession(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>): Record<string, unknown
|
|||
return params;
|
||||
}
|
||||
|
||||
function currentSessionRef(): Record<string, unknown> | undefined {
|
||||
if (currentAgentSessionPath) {
|
||||
return { agent_session_path: currentAgentSessionPath };
|
||||
}
|
||||
if (currentAgentSessionId) {
|
||||
return { agent_session_id: currentAgentSessionId };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function reportSession(): Promise<void> {
|
||||
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<void> {
|
||||
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<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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function (pi) {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
|
|
@ -197,6 +213,7 @@ export default function (pi) {
|
|||
let lastMessage: string | undefined;
|
||||
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let rootSession = false;
|
||||
|
||||
function clearTimer(timer: ReturnType<typeof setTimeout> | 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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<Agent>,
|
||||
) -> 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<Agent>,
|
||||
) -> 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]
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
Loading…
Reference in New Issue