fix: keep user-selected terminal session active instead of auto-handoff (#1562)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-lines -- comprehensive session-state handler tests */
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { registerTaskSessionHandlers } from "./agent-session";
|
||||
import type { StoreApi } from "zustand";
|
||||
@@ -459,6 +460,9 @@ describe("session.state_changed → respects user-pinned session", () => {
|
||||
});
|
||||
|
||||
it("hands off when the pinned active session reaches a terminal state", () => {
|
||||
// Genuine RUNNING→COMPLETED transition: previousState is non-terminal,
|
||||
// so the workflow handoff should still fire even though the session
|
||||
// is pinned.
|
||||
const store = makeStore({
|
||||
tasks: {
|
||||
activeTaskId: "t-1",
|
||||
@@ -490,6 +494,41 @@ describe("session.state_changed → respects user-pinned session", () => {
|
||||
expect(store.getState().setActiveSessionAuto).toHaveBeenCalledWith("t-1", "s-new");
|
||||
expect(store.getState().setActiveSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not hand off when a pinned terminal session receives a replay state_changed", () => {
|
||||
// Replay: the session was already COMPLETED (previousState terminal) and
|
||||
// the backend re-emits the same terminal state. The user clicked this
|
||||
// session open to review it, so the pin must be honored — no handoff.
|
||||
const store = makeStore({
|
||||
tasks: {
|
||||
activeTaskId: "t-1",
|
||||
activeSessionId: "s-old",
|
||||
pinnedSessionId: "s-old",
|
||||
lastSessionByTaskId: {},
|
||||
},
|
||||
taskSessions: {
|
||||
items: { "s-old": { id: "s-old", task_id: "t-1", state: "COMPLETED" } },
|
||||
},
|
||||
taskSessionsByTask: {
|
||||
itemsByTaskId: {
|
||||
"t-1": [
|
||||
{ id: "s-old", task_id: "t-1", state: "COMPLETED", started_at: "", updated_at: "" },
|
||||
{ id: "s-new", task_id: "t-1", state: "STARTING", started_at: "", updated_at: "" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const handler = registerTaskSessionHandlers(store)[STATE_CHANGED_EVENT]!;
|
||||
|
||||
handler({
|
||||
id: "m",
|
||||
type: "notification",
|
||||
action: STATE_CHANGED_EVENT,
|
||||
payload: { task_id: "t-1", session_id: "s-old", new_state: "COMPLETED" },
|
||||
});
|
||||
|
||||
expect(store.getState().setActiveSessionAuto).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function -- test describe block, splitting hurts readability
|
||||
|
||||
@@ -330,12 +330,14 @@ function inheritAgentctlStatus(state: AppState, fromSessionId: string, toSession
|
||||
* 2. The current active session transitions to a terminal state — hand off
|
||||
* to the newest non-terminal session for the same task, if any.
|
||||
*/
|
||||
// eslint-disable-next-line max-params -- newState/previousState/wasKnownToStore are all needed by downstream branches
|
||||
function maybeAdoptSessionOnTransition(
|
||||
store: StoreApi<AppState>,
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
newState: TaskSessionState | undefined,
|
||||
wasKnownToStore: boolean,
|
||||
previousState: TaskSessionState | undefined,
|
||||
): void {
|
||||
const state = store.getState();
|
||||
if (
|
||||
@@ -355,6 +357,18 @@ function maybeAdoptSessionOnTransition(
|
||||
|
||||
const isActive = state.tasks.activeSessionId === sessionId;
|
||||
if (isActive && newState && isTerminalSessionState(newState)) {
|
||||
// When the user clicked open a terminal session (e.g. to review a
|
||||
// completed run), setActiveSession pins it. If the backend then
|
||||
// re-emits the same terminal state_changed (a replay — the previous
|
||||
// stored state was already terminal), honor the pin and do NOT hand
|
||||
// off to a running session. A genuine RUNNING→COMPLETED transition
|
||||
// (previousState non-terminal) still hands off normally.
|
||||
if (
|
||||
state.tasks.pinnedSessionId === sessionId &&
|
||||
previousState &&
|
||||
isTerminalSessionState(previousState)
|
||||
)
|
||||
return;
|
||||
const replacement = pickReplacementSessionId(state, taskId);
|
||||
if (replacement && replacement !== sessionId) {
|
||||
inheritAgentctlStatus(state, sessionId, replacement);
|
||||
@@ -566,7 +580,14 @@ export function registerTaskSessionHandlers(store: StoreApi<AppState>): WsHandle
|
||||
extractContextWindow(store, sessionId, payload);
|
||||
maybePromoteAgentctlReady(store, sessionId, newState, message.timestamp);
|
||||
|
||||
maybeAdoptSessionOnTransition(store, taskId, sessionId, newState, !!existingSession);
|
||||
maybeAdoptSessionOnTransition(
|
||||
store,
|
||||
taskId,
|
||||
sessionId,
|
||||
newState,
|
||||
!!existingSession,
|
||||
existingSession?.state,
|
||||
);
|
||||
|
||||
maybeNotifySessionFailure(store, {
|
||||
taskId,
|
||||
|
||||
Reference in New Issue
Block a user