From de662dc0424b47de4db72df2848cee7f0d54bc2d Mon Sep 17 00:00:00 2001 From: prateek Date: Sun, 15 Feb 2026 05:15:41 +0530 Subject: [PATCH] fix: recognize terminated/done session states and hide terminal for dead sessions (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: recognize terminated/done session states and hide terminal for dead sessions - Add "done" and "terminated" to VALID_STATUSES in session-manager so validateStatus() doesn't fall back to "spawning" for these states - Hide terminal button for terminal-state sessions (no tmux to connect to) - Hide "terminate session" button for already-terminated sessions - Show "restore session" button for terminated/done sessions Co-Authored-By: Claude Sonnet 4.5 * fix: restore activity=exited check for crashed sessions Bugbot caught that the refactor dropped the activity === "exited" condition. When an agent crashes, status stays non-terminal (e.g. "working") but activity becomes "exited" — these need the restore button and should not show terminal/terminate buttons. Co-Authored-By: Claude Sonnet 4.5 * fix: add terminated/done to backend RESTORABLE_STATUSES Frontend shows restore button for terminated/done sessions but the backend restore endpoint only accepted killed/cleanup, returning 409 "Session is not in a terminal state" for the new statuses. Co-Authored-By: Claude Sonnet 4.5 * fix: add type annotations to fix implicit any errors in integration tests Pre-existing issue from package rename — callback parameters in .find() lost type inference. Add explicit type annotations. Co-Authored-By: Claude Sonnet 4.5 * fix: filter orchestrator session from SSR page The API route filtered it but the SSR path in page.tsx did not, causing the orchestrator to appear as a session card. Co-Authored-By: Claude Sonnet 4.5 * fix: make orchestrator session name dynamic using prefix convention Use endsWith("-orchestrator") instead of hardcoded "orchestrator" to support project-prefixed names like "ao-orchestrator". Pass orchestratorId from SSR to Dashboard so the terminal button links to the correct session. Co-Authored-By: Claude Sonnet 4.5 * fix: update stale @agent-orchestrator/core imports to @composio/ao-core Package was renamed in PR #32 but these two files were missed. Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Claude Sonnet 4.5 --- packages/core/src/session-manager.ts | 2 + .../src/tracker-linear.integration.test.ts | 2 +- .../src/workspace-clone.integration.test.ts | 4 +- .../workspace-worktree.integration.test.ts | 4 +- .../app/api/sessions/[id]/restore/route.ts | 2 +- packages/web/src/app/api/sessions/route.ts | 6 +-- packages/web/src/app/page.tsx | 9 +++- packages/web/src/components/Dashboard.tsx | 17 ++++--- packages/web/src/components/SessionCard.tsx | 49 ++++++++++--------- .../web/src/lib/__tests__/serialize.test.ts | 2 +- 10 files changed, 56 insertions(+), 41 deletions(-) diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index ffe624ae5..26adb760c 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -83,6 +83,8 @@ const VALID_STATUSES: ReadonlySet = new Set([ "stuck", "errored", "killed", + "done", + "terminated", ]); /** Validate and normalize a status string. */ diff --git a/packages/integration-tests/src/tracker-linear.integration.test.ts b/packages/integration-tests/src/tracker-linear.integration.test.ts index 62cdc1314..a0aa22a07 100644 --- a/packages/integration-tests/src/tracker-linear.integration.test.ts +++ b/packages/integration-tests/src/tracker-linear.integration.test.ts @@ -231,7 +231,7 @@ describe.skipIf(!canRun)("tracker-linear (integration)", () => { project, ); - const found = issues.find((i) => i.id === issueIdentifier); + const found = issues.find((i: { id: string }) => i.id === issueIdentifier); expect(found).toBeDefined(); expect(found!.title).toContain("[AO Integration Test]"); }); diff --git a/packages/integration-tests/src/workspace-clone.integration.test.ts b/packages/integration-tests/src/workspace-clone.integration.test.ts index d5249c291..4cd6fccee 100644 --- a/packages/integration-tests/src/workspace-clone.integration.test.ts +++ b/packages/integration-tests/src/workspace-clone.integration.test.ts @@ -95,7 +95,7 @@ describe("workspace-clone (integration)", () => { it("lists the clone", async () => { const list = await workspace.list("inttest"); expect(list.length).toBeGreaterThanOrEqual(1); - const found = list.find((w) => w.sessionId === "session-1"); + const found = list.find((w: { sessionId: string }) => w.sessionId === "session-1"); expect(found).toBeDefined(); expect(found!.branch).toBe("feat/test-branch"); }); @@ -119,7 +119,7 @@ describe("workspace-clone (integration)", () => { it("list returns empty after destroy", async () => { const list = await workspace.list("inttest"); - const found = list.find((w) => w.sessionId === "session-1"); + const found = list.find((w: { sessionId: string }) => w.sessionId === "session-1"); expect(found).toBeUndefined(); }); }); diff --git a/packages/integration-tests/src/workspace-worktree.integration.test.ts b/packages/integration-tests/src/workspace-worktree.integration.test.ts index cc35f264e..4910e6e05 100644 --- a/packages/integration-tests/src/workspace-worktree.integration.test.ts +++ b/packages/integration-tests/src/workspace-worktree.integration.test.ts @@ -89,7 +89,7 @@ describe("workspace-worktree (integration)", () => { it("lists the worktree", async () => { const list = await workspace.list("inttest"); expect(list.length).toBeGreaterThanOrEqual(1); - const found = list.find((w) => w.sessionId === "session-1"); + const found = list.find((w: { sessionId: string }) => w.sessionId === "session-1"); expect(found).toBeDefined(); expect(found!.branch).toBe("feat/test-branch"); }); @@ -124,7 +124,7 @@ describe("workspace-worktree (integration)", () => { it("list returns empty after destroy", async () => { const list = await workspace.list("inttest"); - const found = list.find((w) => w.sessionId === "session-1"); + const found = list.find((w: { sessionId: string }) => w.sessionId === "session-1"); expect(found).toBeUndefined(); }); }); diff --git a/packages/web/src/app/api/sessions/[id]/restore/route.ts b/packages/web/src/app/api/sessions/[id]/restore/route.ts index c31f999ca..caa0b3fc5 100644 --- a/packages/web/src/app/api/sessions/[id]/restore/route.ts +++ b/packages/web/src/app/api/sessions/[id]/restore/route.ts @@ -4,7 +4,7 @@ import { getServices } from "@/lib/services"; import { sessionToDashboard } from "@/lib/serialize"; /** Terminal states that can be restored */ -const RESTORABLE_STATUSES = new Set(["killed", "cleanup"]); +const RESTORABLE_STATUSES = new Set(["killed", "cleanup", "terminated", "done"]); const RESTORABLE_ACTIVITIES = new Set(["exited"]); /** Statuses that must never be restored (e.g. already merged) */ diff --git a/packages/web/src/app/api/sessions/route.ts b/packages/web/src/app/api/sessions/route.ts index b717cdef2..9e18c10f9 100644 --- a/packages/web/src/app/api/sessions/route.ts +++ b/packages/web/src/app/api/sessions/route.ts @@ -1,4 +1,4 @@ -import type { Session, ProjectConfig } from "@agent-orchestrator/core"; +import type { Session, ProjectConfig } from "@composio/ao-core"; import { NextResponse } from "next/server"; import { getServices, getSCM, getTracker } from "@/lib/services"; import { sessionToDashboard, enrichSessionPR, enrichSessionIssue, computeStats } from "@/lib/serialize"; @@ -29,8 +29,8 @@ export async function GET() { const { config, registry, sessionManager } = await getServices(); const coreSessions = await sessionManager.list(); - // Filter out special orchestrator session - it's not a worker session - const workerSessions = coreSessions.filter((s) => s.id !== "orchestrator"); + // Filter out orchestrator sessions — they get their own button, not a card + const workerSessions = coreSessions.filter((s) => !s.id.endsWith("-orchestrator")); const dashboardSessions = workerSessions.map(sessionToDashboard); // Enrich issue labels using tracker plugin (synchronous) diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index 90eedbf31..488ffabe6 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -8,9 +8,14 @@ export const dynamic = "force-dynamic"; export default async function Home() { let sessions: DashboardSession[] = []; + let orchestratorId: string | null = null; try { const { config, registry, sessionManager } = await getServices(); - const coreSessions = await sessionManager.list(); + const allSessions = await sessionManager.list(); + // Find and filter out orchestrator sessions — they get their own button, not a card + const orchSession = allSessions.find((s) => s.id.endsWith("-orchestrator")); + if (orchSession) orchestratorId = orchSession.id; + const coreSessions = allSessions.filter((s) => !s.id.endsWith("-orchestrator")); sessions = coreSessions.map(sessionToDashboard); // Enrich issue labels using tracker plugin (synchronous) @@ -97,5 +102,5 @@ export default async function Home() { // Config not found or services unavailable — show empty dashboard } - return ; + return ; } diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx index 04ce18fed..01e618a64 100644 --- a/packages/web/src/components/Dashboard.tsx +++ b/packages/web/src/components/Dashboard.tsx @@ -14,9 +14,10 @@ import { PRTableRow } from "./PRStatus"; interface DashboardProps { sessions: DashboardSession[]; stats: DashboardStats; + orchestratorId?: string | null; } -export function Dashboard({ sessions, stats }: DashboardProps) { +export function Dashboard({ sessions, stats, orchestratorId }: DashboardProps) { const grouped = useMemo(() => { const zones: Record = { merge: [], @@ -86,12 +87,14 @@ export function Dashboard({ sessions, stats }: DashboardProps) { Agent Orchestrator diff --git a/packages/web/src/components/SessionCard.tsx b/packages/web/src/components/SessionCard.tsx index e71fc53c5..113852218 100644 --- a/packages/web/src/components/SessionCard.tsx +++ b/packages/web/src/components/SessionCard.tsx @@ -53,11 +53,14 @@ export function SessionCard({ session, onSend, onKill, onMerge, onRestore }: Ses const alerts = getAlerts(session); const isReadyToMerge = pr?.mergeability.mergeable && pr.state === "open"; - const isRestorable = - (session.status === "killed" || - session.status === "cleanup" || - session.activity === "exited") && - session.status !== "merged"; + const isTerminal = + session.status === "killed" || + session.status === "cleanup" || + session.status === "terminated" || + session.status === "done" || + session.status === "merged" || + session.activity === "exited"; + const isRestorable = isTerminal && session.status !== "merged"; return (
)} - + {!isTerminal && ( + + )}
{/* Meta row: branch + PR pills */} @@ -269,7 +274,7 @@ export function SessionCard({ session, onSend, onKill, onMerge, onRestore }: Ses restore session )} - {!isRestorable && session.status !== "merged" && ( + {!isTerminal && (