parent
cc802c8f9c
commit
ebff34063a
|
|
@ -3,6 +3,7 @@
|
|||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
- OMP now reports a native session reference, so an OMP pane reappears in the Agents panel after exiting and rerunning `omp` in the same pane, and Herdr can resume it with `omp --resume=<session>`. Previously the released lifecycle hook stayed suppressed until a server restart. (#614)
|
||||
- Host terminal color query (OSC 10/11) replies that arrive split at their escape introducer no longer leak as text like `11;rgb:...` into the focused pane, most visible when launching agents that probe terminal colors on startup. (#549)
|
||||
|
||||
## [0.7.0] - 2026-06-15
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ for agents outside the built-in list, herdr still works as a terminal multiplexe
|
|||
|
||||
### direct integrations
|
||||
|
||||
official integrations have two roles. claude code, codex, github copilot cli, devin, droid, qodercli, and cursor agent cli report session identity for native restore, while their state still comes from screen detection. pi, kimi code cli, opencode, kilo code cli, and hermes report both semantic state and session identity. omp reports semantic state without native session restore. install with:
|
||||
official integrations have two roles. claude code, codex, github copilot cli, devin, droid, qodercli, and cursor agent cli report session identity for native restore, while their state still comes from screen detection. pi, omp, kimi code cli, opencode, kilo code cli, and hermes report both semantic state and session identity. install with:
|
||||
|
||||
```bash
|
||||
herdr integration install pi
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ Herdr uses integrations in two different ways:
|
|||
|
||||
Custom socket integrations can also report state when they define state that is not visible in the native terminal UI.
|
||||
|
||||
Some integrations report native agent session references. Herdr uses official session references to resume Claude Code, Codex, Devin CLI, Droid, Kimi Code CLI, Qoder CLI, Cursor Agent CLI, GitHub Copilot CLI, Pi, Hermes Agent, OpenCode, and Kilo Code CLI panes after a Herdr server restart unless `[session] resume_agents_on_restore = false` disables it.
|
||||
Some integrations report native agent session references. Herdr uses official session references to resume Claude Code, Codex, Devin CLI, Droid, Kimi Code CLI, Qoder CLI, Cursor Agent CLI, GitHub Copilot CLI, Pi, OMP, Hermes Agent, OpenCode, and Kilo Code CLI panes after a Herdr server restart unless `[session] resume_agents_on_restore = false` disables it.
|
||||
|
||||
Native session restore requires current Herdr integrations: Pi integration version `2`, Claude Code version `6`, Codex version `5`, GitHub Copilot CLI version `2`, Devin CLI version `1`, Droid version `2`, Kimi Code CLI version `3`, Qoder CLI version `2`, Cursor Agent CLI version `1`, OpenCode version `5`, Kilo Code CLI version `1`, or Hermes Agent version `2`. OMP integration version `2` reports agent state only. Check installed versions with `herdr integration status`.
|
||||
Native session restore requires current Herdr integrations: Pi integration version `2`, OMP version `3`, Claude Code version `6`, Codex version `5`, GitHub Copilot CLI version `2`, Devin CLI version `1`, Droid version `2`, Kimi Code CLI version `3`, Qoder CLI version `2`, Cursor Agent CLI version `1`, OpenCode version `5`, Kilo Code CLI version `1`, or Hermes Agent version `2`. Check installed versions with `herdr integration status`.
|
||||
|
||||
## Pi
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ Herdr writes the bundled extension to:
|
|||
|
||||
If `PI_CODING_AGENT_DIR` is set, Herdr writes to `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts` instead. The extensions directory must already exist. Uninstall removes only that extension file.
|
||||
|
||||
The OMP integration reports `omp` as the agent label through Herdr's socket API. It does not require native process detection for the `omp` executable.
|
||||
The OMP integration reports `omp` as the agent label, lifecycle state, and native session identity through Herdr's socket API. It does not require native process detection for the `omp` executable, and Herdr can resume an OMP pane with `omp --resume=<session>` after a server restart.
|
||||
|
||||
## Claude Code
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ Native session restore requires these Herdr integration versions or newer:
|
|||
| Agent | Minimum Herdr integration version | Resume command |
|
||||
| --- | --- | --- |
|
||||
| Pi | `2` | `pi --session <path-or-id>` |
|
||||
| OMP | `3` | `omp --resume=<path-or-id>` |
|
||||
| Claude Code | `6` | `claude --resume <id>` |
|
||||
| Codex | `5` | `codex resume <id>` |
|
||||
| Cursor Agent CLI | `1` | `cursor-agent --resume <id>` |
|
||||
|
|
@ -79,8 +80,6 @@ Native session restore requires these Herdr integration versions or newer:
|
|||
| Kilo Code CLI | `1` | `kilo --session <id>` |
|
||||
| Hermes Agent | `2` | `hermes --resume <id>` |
|
||||
|
||||
OMP integration version `2` reports agent state, but does not report native session references for restore.
|
||||
|
||||
Run `herdr integration status` to check installed integration versions. Reinstall outdated integrations with `herdr integration install <agent>`.
|
||||
|
||||
Unsupported, missing, invalid, duplicated, or stale session references restore as normal shells in the saved pane directory.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ pub fn session_ref_from_report(
|
|||
return None;
|
||||
}
|
||||
|
||||
if agent == "pi" {
|
||||
if agent == "pi" || agent == "omp" {
|
||||
return _agent_session_path
|
||||
.and_then(AgentSessionRef::path)
|
||||
.or_else(|| agent_session_id.and_then(AgentSessionRef::id));
|
||||
|
|
@ -99,7 +99,7 @@ pub fn session_ref_from_snapshot(
|
|||
return None;
|
||||
}
|
||||
let session_ref = match (agent, kind) {
|
||||
("pi", AgentSessionRefKind::Path) => AgentSessionRef::path(value)?,
|
||||
("pi" | "omp", AgentSessionRefKind::Path) => AgentSessionRef::path(value)?,
|
||||
(_, AgentSessionRefKind::Id) => AgentSessionRef::id(value)?,
|
||||
_ => return None,
|
||||
};
|
||||
|
|
@ -141,6 +141,11 @@ pub fn plan(source: &str, agent: &str, session_ref: &AgentSessionRef) -> Option<
|
|||
("herdr:pi", "pi", AgentSessionRefKind::Path | AgentSessionRefKind::Id) => {
|
||||
vec!["pi".into(), "--session".into(), session_ref.value.clone()]
|
||||
}
|
||||
("herdr:omp", "omp", AgentSessionRefKind::Path | AgentSessionRefKind::Id) => {
|
||||
// omp resume is `-r, --resume=<value>` (ID prefix or path); it has no
|
||||
// `--session` flag, unlike pi.
|
||||
vec!["omp".into(), format!("--resume={}", session_ref.value)]
|
||||
}
|
||||
("herdr:hermes", "hermes", AgentSessionRefKind::Id) => {
|
||||
vec![
|
||||
"hermes".into(),
|
||||
|
|
@ -198,6 +203,7 @@ fn is_official_agent_source(source: &str, agent: &str) -> bool {
|
|||
| ("herdr:devin", "devin")
|
||||
| ("herdr:droid", "droid")
|
||||
| ("herdr:kimi", "kimi")
|
||||
| ("herdr:omp", "omp")
|
||||
| ("herdr:pi", "pi")
|
||||
| ("herdr:hermes", "hermes")
|
||||
| ("herdr:opencode", "opencode")
|
||||
|
|
@ -245,6 +251,7 @@ mod tests {
|
|||
#[test]
|
||||
fn planner_allows_supported_agents() {
|
||||
let pi_session = absolute_test_path("pi-session.jsonl");
|
||||
let omp_session = absolute_test_path("omp-session.jsonl");
|
||||
assert_eq!(
|
||||
plan(
|
||||
"herdr:claude",
|
||||
|
|
@ -315,6 +322,16 @@ mod tests {
|
|||
.argv,
|
||||
vec!["pi", "--session", pi_session.as_str()]
|
||||
);
|
||||
assert_eq!(
|
||||
plan(
|
||||
"herdr:omp",
|
||||
"omp",
|
||||
&AgentSessionRef::path(&omp_session).unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.argv,
|
||||
vec!["omp", format!("--resume={omp_session}").as_str()]
|
||||
);
|
||||
assert_eq!(
|
||||
plan(
|
||||
"herdr:hermes",
|
||||
|
|
@ -385,8 +402,9 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn report_ref_prefers_pi_path_and_validates_values() {
|
||||
fn report_ref_prefers_pi_and_omp_paths_and_validates_values() {
|
||||
let pi_session = absolute_test_path("pi-session.jsonl");
|
||||
let omp_session = absolute_test_path("omp-session.jsonl");
|
||||
let claude_session = absolute_test_path("claude-session");
|
||||
let copilot_session = absolute_test_path("copilot-session");
|
||||
let session_ref = session_ref_from_report(
|
||||
|
|
@ -405,6 +423,35 @@ mod tests {
|
|||
.is_none()
|
||||
);
|
||||
assert!(session_ref_from_report("custom:pi", "pi", Some("pi-id".into()), None).is_none());
|
||||
|
||||
let session_ref = session_ref_from_report(
|
||||
"herdr:omp",
|
||||
"omp",
|
||||
Some("omp-id".into()),
|
||||
Some(omp_session.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(session_ref.kind, AgentSessionRefKind::Path);
|
||||
assert_eq!(session_ref.value, omp_session);
|
||||
|
||||
let session_ref =
|
||||
session_ref_from_report("herdr:omp", "omp", Some("omp-id".into()), None).unwrap();
|
||||
assert_eq!(session_ref.kind, AgentSessionRefKind::Id);
|
||||
assert_eq!(session_ref.value, "omp-id");
|
||||
let session_ref = session_ref_from_report(
|
||||
"herdr:omp",
|
||||
"omp",
|
||||
Some("omp-id".into()),
|
||||
Some("relative.jsonl".into()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(session_ref.kind, AgentSessionRefKind::Id);
|
||||
assert_eq!(session_ref.value, "omp-id");
|
||||
assert!(
|
||||
session_ref_from_report("herdr:omp", "omp", None, Some("relative.jsonl".into()))
|
||||
.is_none()
|
||||
);
|
||||
|
||||
assert!(
|
||||
session_ref_from_report("herdr:claude", "claude", None, Some(claude_session)).is_none()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// managed by herdr; reinstalling or updating the integration overwrites this file.
|
||||
// add custom hooks/plugins beside this file instead of editing it.
|
||||
// HERDR_INTEGRATION_ID=omp
|
||||
// HERDR_INTEGRATION_VERSION=2
|
||||
// HERDR_INTEGRATION_VERSION=3
|
||||
// @ts-nocheck
|
||||
|
||||
import { createConnection } from "node:net";
|
||||
|
|
@ -53,12 +53,41 @@ const retryGraceMs = parseDurationEnv("HERDR_OMP_RETRY_GRACE_MS", 2500);
|
|||
const retryableErrorPattern =
|
||||
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
|
||||
let reportSeq = Date.now() * 1000;
|
||||
let currentAgentSessionId: string | undefined;
|
||||
let currentAgentSessionPath: string | undefined;
|
||||
|
||||
function nextReportSeq(): number {
|
||||
reportSeq += 1;
|
||||
return reportSeq;
|
||||
}
|
||||
|
||||
function updateSessionRef(ctx: any): void {
|
||||
try {
|
||||
const file = ctx?.sessionManager?.getSessionFile?.();
|
||||
currentAgentSessionPath =
|
||||
typeof file === "string" && file.startsWith("/") ? file : undefined;
|
||||
} catch {
|
||||
currentAgentSessionPath = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = ctx?.sessionManager?.getSessionId?.();
|
||||
currentAgentSessionId = typeof id === "string" && id.length > 0 ? id : undefined;
|
||||
} catch {
|
||||
currentAgentSessionId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function withSessionRef(params: Record<string, unknown>): Record<string, unknown> {
|
||||
if (currentAgentSessionPath) {
|
||||
return { ...params, agent_session_path: currentAgentSessionPath };
|
||||
}
|
||||
if (currentAgentSessionId) {
|
||||
return { ...params, agent_session_id: currentAgentSessionId };
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
function parseDurationEnv(name: string, fallback: number): number {
|
||||
const raw = process.env[name];
|
||||
if (!raw) {
|
||||
|
|
@ -75,14 +104,14 @@ function sendState(state: AgentState, message?: string, seq = nextReportSeq()):
|
|||
return sendRequest({
|
||||
id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
||||
method: "pane.report_agent",
|
||||
params: {
|
||||
params: withSessionRef({
|
||||
pane_id: paneId,
|
||||
source,
|
||||
agent: "omp",
|
||||
state,
|
||||
message,
|
||||
seq,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -201,9 +230,9 @@ export default function (pi) {
|
|||
return { state: "idle" as const, message: undefined };
|
||||
}
|
||||
|
||||
function publishState() {
|
||||
function publishState(force = false) {
|
||||
const next = desiredState();
|
||||
if (next.state === lastState && next.message === lastMessage) {
|
||||
if (!force && next.state === lastState && next.message === lastMessage) {
|
||||
return;
|
||||
}
|
||||
lastState = next.state;
|
||||
|
|
@ -253,8 +282,9 @@ export default function (pi) {
|
|||
publishState();
|
||||
});
|
||||
|
||||
pi.on("session_start", () => {
|
||||
publishState();
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
updateSessionRef(ctx);
|
||||
publishState(true);
|
||||
});
|
||||
|
||||
pi.on("agent_start", () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const PI_EXTENSION_ASSET: &str = include_str!("assets/pi/herdr-agent-state.ts");
|
|||
const PI_INTEGRATION_VERSION: u32 = 2;
|
||||
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 = 2;
|
||||
const OMP_INTEGRATION_VERSION: u32 = 3;
|
||||
const PI_CODING_AGENT_DIR_ENV_VAR: &str = "PI_CODING_AGENT_DIR";
|
||||
const CLAUDE_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
|
||||
"herdr-agent-state.ps1"
|
||||
|
|
@ -5969,6 +5969,9 @@ mod tests {
|
|||
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"));
|
||||
|
|
|
|||
|
|
@ -1493,6 +1493,52 @@ mod tests {
|
|||
assert_eq!(terminal.state, AgentState::Working);
|
||||
}
|
||||
|
||||
// omp has no Agent identity (the process is pi), so its release suppression
|
||||
// can only be cleared by a fresh session ref, never by process detection.
|
||||
// Regression for #614: a same-pane restart must reacquire lifecycle authority.
|
||||
#[test]
|
||||
fn omp_reacquires_full_lifecycle_hook_after_release_with_fresh_session_ref() {
|
||||
let mut terminal = test_terminal();
|
||||
terminal.set_hook_authority_with_session_ref(
|
||||
"herdr:omp".into(),
|
||||
"omp".into(),
|
||||
AgentState::Working,
|
||||
None,
|
||||
None,
|
||||
crate::agent_resume::AgentSessionRef::id("omp-old"),
|
||||
Some(20),
|
||||
);
|
||||
terminal.release_agent("herdr:omp", "omp", Some(21));
|
||||
|
||||
// A late report from the released run keeps its old session ref and stays
|
||||
// suppressed, so a just-exited omp cannot resurrect the pane.
|
||||
let stale = terminal.set_hook_authority_with_session_ref(
|
||||
"herdr:omp".into(),
|
||||
"omp".into(),
|
||||
AgentState::Working,
|
||||
None,
|
||||
None,
|
||||
crate::agent_resume::AgentSessionRef::id("omp-old"),
|
||||
Some(22),
|
||||
);
|
||||
assert!(stale.is_none());
|
||||
assert!(terminal.hook_authority.is_none());
|
||||
|
||||
// A fresh omp run carries a new session ref and reacquires authority.
|
||||
let fresh = terminal.set_hook_authority_with_session_ref(
|
||||
"herdr:omp".into(),
|
||||
"omp".into(),
|
||||
AgentState::Working,
|
||||
None,
|
||||
None,
|
||||
crate::agent_resume::AgentSessionRef::id("omp-new"),
|
||||
Some(23),
|
||||
);
|
||||
assert!(fresh.is_some());
|
||||
assert!(terminal.hook_authority.is_some());
|
||||
assert_eq!(terminal.state, AgentState::Working);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visible_blocker_overrides_non_blocked_hook_for_same_agent() {
|
||||
let mut terminal = test_terminal();
|
||||
|
|
|
|||
Loading…
Reference in New Issue