diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-22 21:54:49 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-22 21:54:49 +0300 |
| commit | f23e3779dc6827739fed34437519f548fcb9d76b (patch) | |
| tree | 648903975c2cc65281d82fbc2d6c1718aa10f96e | |
| parent | 73d17682ce2e8a3c5da6888f3b4d05abf1523929 (diff) | |
refactor: clarify opencode event state mapping
replaces contributions from soar
| -rw-r--r-- | src/integration/assets/opencode/herdr-agent-state.js | 61 | ||||
| -rw-r--r-- | src/integration/assets/opencode/herdr-agent-state.test.ts | 51 |
2 files changed, 77 insertions, 35 deletions
diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js index 27ea7ee5..76265050 100644 --- a/src/integration/assets/opencode/herdr-agent-state.js +++ b/src/integration/assets/opencode/herdr-agent-state.js @@ -12,10 +12,16 @@ let reportSeq = Date.now() * 1000; let requestChain = Promise.resolve(); let reportedRootSessionID; -// Subagent (task tool) sessions carry a parentID; the main agent session does -// not. Their lifecycle events would otherwise clobber the pane's real state, so -// learn child session ids from session.created/updated and drop their reports. +// Track child sessions so their events cannot replace the pane's root session. +// Their user prompts still project state without attaching the child session id. const childSessions = new Set(); +const CHILD_EVENT_STATES = new Map([ + ["permission.asked", "blocked"], + ["question.asked", "blocked"], + ["permission.replied", "working"], + ["question.replied", "working"], + ["question.rejected", "working"], +]); function nextReportSeq() { reportSeq += 1; @@ -28,24 +34,22 @@ function sessionIDFromProperties(properties) { : undefined; } +const SESSION_STATE_BY_STATUS = new Map([ + ["idle", "idle"], + ["active", "working"], + ["busy", "working"], + ["pending", "working"], + ["retry", "working"], + ["running", "working"], + ["streaming", "working"], + ["working", "working"], +]); + function stateFromSessionStatus(status) { - // session.status carries { type: "idle" | "busy" | "retry" }; older builds used a bare string. const kind = typeof status === "string" ? status : status?.type; - if (typeof kind !== "string") return undefined; - switch (kind.toLowerCase()) { - case "idle": - return "idle"; - case "active": - case "busy": - case "pending": - case "running": - case "streaming": - case "working": - case "retry": - return "working"; - default: - return undefined; - } + return typeof kind === "string" + ? SESSION_STATE_BY_STATUS.get(kind.toLowerCase()) + : undefined; } function request(method, params) { @@ -144,22 +148,9 @@ export const HerdrAgentStatePlugin = async () => { childSessions.add(info.id); } if (sessionID && childSessions.has(sessionID)) { - // Child session events are dropped so they cannot clobber the pane's - // root-agent state, but a subagent waiting on the user must still - // surface as blocked (and clear once answered). Report state only, - // without an agent_session_id, so the pane keeps the root session. - switch (type) { - case "permission.asked": - case "question.asked": - await reportState("blocked"); - break; - case "permission.replied": - case "question.replied": - case "question.rejected": - await reportState("working"); - break; - default: - break; + const state = CHILD_EVENT_STATES.get(type); + if (state) { + await reportState(state); } return; } diff --git a/src/integration/assets/opencode/herdr-agent-state.test.ts b/src/integration/assets/opencode/herdr-agent-state.test.ts index d8d6d14d..be21fd71 100644 --- a/src/integration/assets/opencode/herdr-agent-state.test.ts +++ b/src/integration/assets/opencode/herdr-agent-state.test.ts @@ -114,6 +114,57 @@ test("suppresses redundant same-session updates", async () => { expect(requests.map(requestSessionID)).toEqual(["root-session", "replacement-session"]); }); +test("reports retry status as working", async () => { + const plugin = await loadPlugin(); + + await plugin.event({ + event: { + type: "session.status", + properties: { sessionID: "root-session", status: { type: "retry" } }, + }, + }); + + expect(requests.map(requestMethod)).toEqual(["pane.report_agent"]); + expect(requests.map(requestState)).toEqual(["working"]); + expect(requests.map(requestSessionID)).toEqual(["root-session"]); +}); + +test("reports child prompts without replacing the root session", async () => { + const plugin = await loadPlugin(); + + await plugin.event({ + event: { + type: "session.created", + properties: { + sessionID: "child-session", + info: { id: "child-session", parentID: "root-session" }, + }, + }, + }); + + for (const type of ["permission.asked", "question.asked"]) { + await plugin.event({ event: { type, properties: { sessionID: "child-session" } } }); + } + for (const type of ["permission.replied", "question.replied", "question.rejected"]) { + await plugin.event({ event: { type, properties: { sessionID: "child-session" } } }); + } + + expect(requests.map(requestState)).toEqual([ + "blocked", + "blocked", + "working", + "working", + "working", + ]); + expect(requests.map(requestSessionID)).toEqual([ + undefined, + undefined, + undefined, + undefined, + undefined, + ]); +}); + function requestMethod(request: unknown): unknown { return isRecord(request) ? request.method : undefined; } |
