From 59be8975a463e92ead1fc5ea4c120b10de089eec Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 5 Nov 2026 18:49:12 +0700 Subject: [PATCH] Dismiss a failed agent's red dot by reading it (#8831) * fix(sidebar): clear the failed status once the error is read Reading an errored agent already cleared its error attention, but the state bucket also mapped lifecycle status "error" to failed. Lifecycle only leaves "error" when the next turn starts, so the red dot and the Failed group stuck until the agent was prompted again. The failed bucket now follows unread error attention, the same way the green dot follows unread finished attention; lifecycle status stays "error". Because failed now depends on attention, a failure must replace unread finished attention, otherwise a reload that fails on an unread agent would show green. * fix(sidebar): keep failed subagents red and let read failures be marked unread The failed bucket now follows unread error attention, which broke three places that relied on lifecycle "error" alone: - Subagent track rows and provider-subagent tabs have no read state of their own, so failed children lost their red mark. They now map the child's lifecycle directly. - A read errored workspace became "done" and offered Mark as unread, but the daemon only accepted idle or closed agents. Marking it unread now restores error attention. - A reload that failed again while the lifecycle was already "error" raised no new attention. A failed reload now raises error attention directly instead of relying on a lifecycle transition. --- docs/agent-lifecycle.md | 13 ++- .../src/panels/provider-subagent-panel.tsx | 7 +- packages/app/src/subagents/index.ts | 1 + .../app/src/subagents/track-presentation.ts | 18 +++- .../utils/workspace-agent-activity.test.ts | 2 + .../protocol/src/agent-state-bucket.test.ts | 35 ++++++- packages/protocol/src/agent-state-bucket.ts | 10 +- .../src/server/agent/agent-manager.test.ts | 93 +++++++++++++++++++ .../server/src/server/agent/agent-manager.ts | 55 +++++++---- packages/server/src/server/session.ts | 7 +- .../src/server/session.workspaces.test.ts | 50 +++++++++- .../src/server/workspace-directory.test.ts | 15 +++ 12 files changed, 267 insertions(+), 39 deletions(-) diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index d8ee067c6c..1d0b29b018 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -305,9 +305,9 @@ Workspace status is an aggregate activity signal computed **per `workspaceId`**. Running provider-native subagents contribute `running` to the workspace owned by their parent agent. Their completed, failed, and canceled states stay in the parent's subagents track. -A finished workspace can be marked unread after it has been reviewed. The daemon restores -`finished` attention on its newest eligible workspace-root agent without sending a new completion -notification. Opening the workspace clears that attention through the normal focus flow. +A finished or failed workspace can be marked unread after it has been reviewed. The daemon restores +attention on its newest eligible workspace-root agent, `error` for an agent whose lifecycle is +`error` and `finished` otherwise, without sending a new notification. Opening the workspace clears that attention through the normal focus flow. Attention is set by the agent finishing or failing and cleared by the client's `workspace.clear_attention`, which fires when the user reads the chat. Loading an agent's runtime is @@ -316,6 +316,13 @@ untouched. Forging either makes a background resume look like the user read a wo agent worked in it just now, which rewrites the sidebar timestamp permanently — persisted `updatedAt` is what workspace `statusEnteredAt` is re-derived from on the next daemon start. +The `failed` bucket follows unread `error` attention, not lifecycle status. An agent stays `error` +until its next turn starts, so reading the chat dismisses the red dot the same way it dismisses the +green one. For the same reason every new failure raises `error` attention, including a failed reload +that leaves the lifecycle at `error`, and replaces unread `finished` attention; otherwise the failed +agent would show the green dot or none. Subagents track marks are the exception: they follow the +child's lifecycle, because the track is where a failed child stays visible. + ## The subagents track The track is a pill at the foot of an agent's pane (`packages/app/src/subagents/track.tsx`): a count you can read at a glance, and a panel behind it — a popover on wide screens, a sheet on compact ones — holding the rows. It floats over the transcript rather than sitting in a band above the composer, so the timeline scrolls underneath it; `packages/app/src/panels/agent-tracks.tsx` owns that placement, and the pill frame is shared with the task list in `packages/app/src/composer/tracks.tsx`. diff --git a/packages/app/src/panels/provider-subagent-panel.tsx b/packages/app/src/panels/provider-subagent-panel.tsx index 99a38c2f8b..377d5331c8 100644 --- a/packages/app/src/panels/provider-subagent-panel.tsx +++ b/packages/app/src/panels/provider-subagent-panel.tsx @@ -31,7 +31,7 @@ import { import { useTranslation } from "react-i18next"; import type { PendingPermission } from "@/types/shared"; import type { StreamItem } from "@/types/stream"; -import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state"; +import { deriveSubagentStatusBucket } from "@/subagents"; import { TIMELINE_FETCH_PAGE_SIZE } from "@/timeline/timeline-fetch-policy"; import type { TurnPresentation } from "@/timeline/turn-liveness"; import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model"; @@ -107,10 +107,7 @@ function useProviderSubagentDescriptor( titleState: descriptor ? "ready" : "loading", icon: useProviderIcon(provider, context.serverId), statusBucket: descriptor - ? deriveSidebarStateBucket({ - status: providerSubagentLifecycleStatus(descriptor.status), - requiresAttention: descriptor.status === "failed", - }) + ? deriveSubagentStatusBucket(providerSubagentLifecycleStatus(descriptor.status)) : null, }; } diff --git a/packages/app/src/subagents/index.ts b/packages/app/src/subagents/index.ts index 0b72ccfcf5..cd8ee753fb 100644 --- a/packages/app/src/subagents/index.ts +++ b/packages/app/src/subagents/index.ts @@ -10,3 +10,4 @@ export { type UseArchiveFinishedSubagentsInput, } from "./use-archive-finished"; export { useSubagentSheetTab } from "./sheet-tab"; +export { deriveSubagentStatusBucket } from "./track-presentation"; diff --git a/packages/app/src/subagents/track-presentation.ts b/packages/app/src/subagents/track-presentation.ts index d95082ca39..5a1d671339 100644 --- a/packages/app/src/subagents/track-presentation.ts +++ b/packages/app/src/subagents/track-presentation.ts @@ -1,7 +1,8 @@ import type { TFunction } from "i18next"; import type { ComposerTrackPillSegment } from "@/composer/tracks"; import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; -import { deriveSidebarStateBucket, STATUS_BUCKET_ORDER } from "@/utils/sidebar-agent-state"; +import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle"; +import { STATUS_BUCKET_ORDER } from "@/utils/sidebar-agent-state"; import type { SubagentRow } from "./select"; import { isFinishedSubagent } from "./archive-finished"; import { providerSubagentLifecycleStatus } from "./provider-store"; @@ -14,6 +15,16 @@ function presentationStatus(row: SubagentRow) { return providerSubagentLifecycleStatus(row.status); } +/** + * A child's mark reports what it is doing, not whether anyone read it: the track is where a + * failed child stays visible after the parent's own attention is cleared. + */ +export function deriveSubagentStatusBucket(status: AgentLifecycleStatus): SidebarStateBucket { + if (status === "error") return "failed"; + if (status === "running") return "running"; + return "done"; +} + export interface SubagentRowPresentationData { key: string; kind: "agent"; @@ -38,10 +49,7 @@ export function buildSubagentRowPresentationData(row: SubagentRow): SubagentRowP label: label ?? "", subtitle: subtitle ?? "", titleState: label ? "ready" : "loading", - statusBucket: deriveSidebarStateBucket({ - status, - requiresAttention: false, - }), + statusBucket: deriveSubagentStatusBucket(status), }; } diff --git a/packages/app/src/utils/workspace-agent-activity.test.ts b/packages/app/src/utils/workspace-agent-activity.test.ts index 66cfcd758c..a539a3881a 100644 --- a/packages/app/src/utils/workspace-agent-activity.test.ts +++ b/packages/app/src/utils/workspace-agent-activity.test.ts @@ -93,6 +93,8 @@ describe("workspace agent activity index", () => { workspaceId: "workspace-error", status: "error", turn: { phase: "idle", cancellationRequestId: null }, + requiresAttention: true, + attentionReason: "error", updatedAt: "2026-01-01T00:00:00.000Z", }), ], diff --git a/packages/protocol/src/agent-state-bucket.test.ts b/packages/protocol/src/agent-state-bucket.test.ts index 3c5d281b39..89d1ef93ab 100644 --- a/packages/protocol/src/agent-state-bucket.test.ts +++ b/packages/protocol/src/agent-state-bucket.test.ts @@ -39,6 +39,28 @@ describe("deriveAgentStateBucket", () => { ).toBe("failed"); }); + it("treats an errored agent whose error was read as done", () => { + expect( + deriveAgentStateBucket({ + status: "error", + pendingPermissionCount: 0, + requiresAttention: false, + attentionReason: null, + }), + ).toBe("done"); + }); + + it("treats an errored agent with unread error attention as failed", () => { + expect( + deriveAgentStateBucket({ + status: "error", + pendingPermissionCount: 0, + requiresAttention: true, + attentionReason: "error", + }), + ).toBe("failed"); + }); + it("treats unread finished agents as attention", () => { expect( deriveAgentStateBucket({ @@ -83,10 +105,21 @@ describe("getAgentStatusPriority", () => { it("prioritizes pending permissions before errors and running agents", () => { const permission = getAgentStatusPriority({ status: "running", pendingPermissionCount: 1 }); expect(permission).toBeLessThan( - getAgentStatusPriority({ status: "error", pendingPermissionCount: 0 }), + getAgentStatusPriority({ + status: "error", + pendingPermissionCount: 0, + requiresAttention: true, + attentionReason: "error", + }), ); expect(permission).toBeLessThan( getAgentStatusPriority({ status: "running", pendingPermissionCount: 0 }), ); }); + + it("ranks an errored agent with a read error alongside completed agents", () => { + expect(getAgentStatusPriority({ status: "error", requiresAttention: false })).toBe( + getAgentStatusPriority({ status: "idle" }), + ); + }); }); diff --git a/packages/protocol/src/agent-state-bucket.ts b/packages/protocol/src/agent-state-bucket.ts index 2973152f1d..746d70a4e2 100644 --- a/packages/protocol/src/agent-state-bucket.ts +++ b/packages/protocol/src/agent-state-bucket.ts @@ -19,11 +19,17 @@ const WORKSPACE_STATE_BUCKET_PRIORITY = { done: 4, } as const satisfies Record; +// An error is a notification like a finish: once the user has seen it, the agent stops showing as +// failed even though its lifecycle stays "error" until the next turn starts. +function hasUnreadError(input: AgentStateBucketInput): boolean { + return input.requiresAttention === true && input.attentionReason === "error"; +} + export function deriveAgentStateBucket(input: AgentStateBucketInput): WorkspaceStateBucket { if ((input.pendingPermissionCount ?? 0) > 0 || input.attentionReason === "permission") { return "needs_input"; } - if (input.status === "error" || input.attentionReason === "error") { + if (hasUnreadError(input)) { return "failed"; } if (input.status === "running") { @@ -43,7 +49,7 @@ export function getAgentStatusPriority(input: AgentStateBucketInput): number { if ((input.pendingPermissionCount ?? 0) > 0 || input.attentionReason === "permission") { return 0; } - if (input.status === "error" || input.attentionReason === "error") { + if (hasUnreadError(input)) { return 1; } if (input.status === "running") { diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 57e5b0df20..6c7396d657 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -2801,6 +2801,53 @@ test("closing during reload leaves the replacement closed", async () => { } }); +test("every failed reload raises error attention, replacing unread finished attention", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-error-attention-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const client = new HeldReloadCloseClient(); + const attentionReasons: Array<"finished" | "error" | "permission"> = []; + const manager = new AgentManager({ + clients: { codex: client }, + registry: storage, + logger, + rescueTimeouts: { reloadSessionCloseMs: 10 }, + onAgentAttention: ({ reason }) => attentionReasons.push(reason), + }); + try { + const created = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + await manager.markAgentUnread(created.id); + expect(manager.getAgent(created.id)?.attention).toMatchObject({ + attentionReason: "finished", + }); + + await expect(manager.reloadAgentSession(created.id)).rejects.toThrow("Timed out closing"); + + expect(manager.getAgent(created.id)?.lifecycle).toBe("error"); + expect(manager.getAgent(created.id)?.attention).toMatchObject({ + requiresAttention: true, + attentionReason: "error", + }); + expect(attentionReasons).toEqual(["error"]); + + await manager.clearAgentAttention(created.id); + await expect(manager.reloadAgentSession(created.id)).rejects.toThrow("Timed out closing"); + expect(manager.getAgent(created.id)?.attention).toMatchObject({ + requiresAttention: true, + attentionReason: "error", + }); + expect(attentionReasons).toEqual(["error", "error"]); + + client.finishClosing(); + await manager.closeAgent(created.id); + } finally { + client.finishClosing(); + await storage.flush(); + rmSync(workdir, { recursive: true, force: true }); + } +}); + test("retrying a timed-out reload waits for the original close to finish", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-late-close-")); const storage = new AgentStorage(join(workdir, "agents"), logger); @@ -5807,6 +5854,52 @@ test("archiveSnapshot dispatches archived state for stored-only agents", async ( expect(last.lifecycle).toBe("closed"); }); +test("markAgentUnread restores error attention on a read errored agent", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-mark-unread-error-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + class ErrorClient extends TestAgentClient { + readonly session = new TestAgentSession({ provider: "codex", cwd: workdir }); + + override async createSession(): Promise { + return this.session; + } + } + const client = new ErrorClient(); + const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger }); + onTestFinished(async () => { + await manager.closeAgent(agent.id); + await manager.flush(); + rmSync(workdir, { recursive: true, force: true }); + }); + const agent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + const running = waitForAgentLifecycle(manager, agent.id, "running"); + client.session.pushEvent({ type: "turn_started", provider: "codex", turnId: "failed-turn" }); + await running; + const failed = waitForAgentLifecycle(manager, agent.id, "error"); + client.session.pushEvent({ + type: "turn_failed", + provider: "codex", + turnId: "failed-turn", + error: "Selected model is at capacity.", + }); + await failed; + await manager.clearAgentAttention(agent.id); + + await manager.markAgentUnread(agent.id); + + expect(manager.getAgent(agent.id)?.attention).toMatchObject({ + requiresAttention: true, + attentionReason: "error", + }); + expect(await storage.get(agent.id)).toMatchObject({ + lastStatus: "error", + requiresAttention: true, + attentionReason: "error", + }); +}); + test("markAgentUnread dispatches stored attention to every subscriber", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-mark-unread-dispatch-")); const storage = new AgentStorage(join(workdir, "agents"), logger); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 387d76082d..fea3eed438 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -411,6 +411,13 @@ function raisesAttention(agent: Pick): return !agent.internal && !agent.background; } +/** The attention a read agent gets back when marked unread; null while it is still working. */ +function unreadAttentionReason(status: AgentLifecycleStatus): "finished" | "error" | null { + if (status === "idle" || status === "closed") return "finished"; + if (status === "error") return "error"; + return null; +} + function resolveInitialAttention(input: AttentionState | undefined): AttentionState { if (input == null || !input.requiresAttention) { return { requiresAttention: false }; @@ -1776,6 +1783,7 @@ export class AgentManager { } else if (this.agents.get(agentId) === existing) { existing.lifecycle = "error"; existing.lastError = error instanceof Error ? error.message : String(error); + this.raiseErrorAttention(existing); this.emitState(existing); } throw error; @@ -2433,16 +2441,16 @@ export class AgentManager { async markAgentUnread(agentId: string): Promise { const liveAgent = this.agents.get(agentId); if (liveAgent) { - const isFinished = liveAgent.lifecycle === "idle"; + const attentionReason = unreadAttentionReason(liveAgent.lifecycle); const hasPendingPermissions = liveAgent.pendingPermissions.size > 0; const canMarkUnread = - isFinished && !liveAgent.attention.requiresAttention && !hasPendingPermissions; + attentionReason && !liveAgent.attention.requiresAttention && !hasPendingPermissions; if (!canMarkUnread) { - throw new Error(`Agent is no longer finished and read: ${agentId}`); + throw new Error(`Agent is no longer settled and read: ${agentId}`); } liveAgent.attention = { requiresAttention: true, - attentionReason: "finished", + attentionReason, attentionTimestamp: new Date(), }; await this.persistSnapshot(liveAgent); @@ -2452,18 +2460,18 @@ export class AgentManager { const registry = this.requireRegistry(); const record = await registry.get(agentId); - const hasFinishedStatus = record?.lastStatus === "idle" || record?.lastStatus === "closed"; + const attentionReason = record?.lastStatus ? unreadAttentionReason(record.lastStatus) : null; const canMarkUnread = record && !record.internal && !record.archivedAt && !record.requiresAttention; - if (!canMarkUnread || !hasFinishedStatus) { - throw new Error(`Agent is no longer finished and read: ${agentId}`); + if (!canMarkUnread || !attentionReason) { + throw new Error(`Agent is no longer settled and read: ${agentId}`); } const updatedAt = this.nextStoredUpdatedAt(record); const nextRecord: StoredAgentRecord = { ...record, updatedAt, requiresAttention: true, - attentionReason: "finished", + attentionReason, attentionTimestamp: updatedAt, }; await registry.upsert(nextRecord); @@ -5300,12 +5308,15 @@ export class AgentManager { return; } - // Skip if already requires attention + if (previousStatus !== "error" && currentStatus === "error") { + this.raiseErrorAttention(agent); + return; + } + if (agent.attention.requiresAttention) { return; } - // Check if agent transitioned from running to idle (finished) if (previousStatus === "running" && currentStatus === "idle") { agent.attention = { requiresAttention: true, @@ -5313,19 +5324,25 @@ export class AgentManager { attentionTimestamp: new Date(), }; this.broadcastAgentAttention(agent, "finished"); - return; } + } - // Check if agent entered error state - if (previousStatus !== "error" && currentStatus === "error") { - agent.attention = { - requiresAttention: true, - attentionReason: "error", - attentionTimestamp: new Date(), - }; - this.broadcastAgentAttention(agent, "error"); + // The failed state is only shown while error attention is unread, so every new failure raises + // it, including one that keeps the lifecycle at "error". An error outranks an unread finish, + // which would otherwise make the failure look green. + private raiseErrorAttention(agent: ManagedAgent): void { + if (!raisesAttention(agent)) { return; } + if (agent.attention.requiresAttention && agent.attention.attentionReason !== "finished") { + return; + } + agent.attention = { + requiresAttention: true, + attentionReason: "error", + attentionTimestamp: new Date(), + }; + this.broadcastAgentAttention(agent, "error"); } private enqueueBackgroundPersist(agent: ManagedAgent): void { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 4b260f8550..9ce3fbbcb4 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -7955,7 +7955,10 @@ export class Session { .filter((agent) => !agent.archivedAt && !agent.background) .filter((agent) => agent.workspaceId === workspace.workspaceId) .filter((agent) => resolveWorkspaceRootAgent(agent, agentsById)?.id === agent.id) - .filter((agent) => agent.status === "idle" || agent.status === "closed") + .filter( + (agent) => + agent.status === "idle" || agent.status === "closed" || agent.status === "error", + ) .filter((agent) => agent.requiresAttention !== true) .filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0) .sort( @@ -7964,7 +7967,7 @@ export class Session { ); const candidate = candidates[0]; if (!candidate) { - throw new Error(`Workspace has no finished agent to mark unread: ${workspaceId}`); + throw new Error(`Workspace has no read agent to mark unread: ${workspaceId}`); } await this.agentManager.markAgentUnread(candidate.id); diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 6b1db34449..928131ab3a 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -2123,7 +2123,53 @@ test("workspace mark unread selects a finished cross-workspace child with scoped } }); -test("workspace mark unread rejects workspaces without a finished root agent", async () => { +test("workspace mark unread marks a read errored root agent", async () => { + const emitted: SessionOutboundMessage[] = []; + const workspace = createPersistedWorkspaceRecord({ + workspaceId: REPO_CWD, + projectId: REPO_CWD, + cwd: REPO_CWD, + kind: "directory", + displayName: "repo", + createdAt: "2026-03-30T15:00:00.000Z", + updatedAt: "2026-03-30T15:00:00.000Z", + }); + const markedAgentIds: string[] = []; + const session = createSessionForWorkspaceTests({ + onMessage: (message) => emitted.push(message), + agentManager: { + markAgentUnread: async (agentId: string) => { + markedAgentIds.push(agentId); + }, + }, + }); + session.workspaceRegistry.get = async (id: string) => + id === workspace.workspaceId ? workspace : null; + session.listAgentPayloads = async () => [ + makeAgent({ + id: "errored-agent", + cwd: REPO_CWD, + workspaceId: workspace.workspaceId, + status: "error", + updatedAt: "2026-03-30T16:00:00.000Z", + requiresAttention: false, + }), + ]; + + await session.handleMessage({ + type: "workspace.mark_unread.request", + workspaceId: workspace.workspaceId, + requestId: "req-mark-unread", + }); + + expect(markedAgentIds).toEqual(["errored-agent"]); + expect(findByType(emitted, "workspace.mark_unread.response").payload).toMatchObject({ + markedAgentId: "errored-agent", + success: true, + }); +}); + +test("workspace mark unread rejects workspaces without a read root agent", async () => { const emitted: SessionOutboundMessage[] = []; const workspace = createPersistedWorkspaceRecord({ workspaceId: REPO_CWD, @@ -2150,7 +2196,7 @@ test("workspace mark unread rejects workspaces without a finished root agent", a workspaceId: workspace.workspaceId, markedAgentId: null, success: false, - error: `Workspace has no finished agent to mark unread: ${workspace.workspaceId}`, + error: `Workspace has no read agent to mark unread: ${workspace.workspaceId}`, }); }); diff --git a/packages/server/src/server/workspace-directory.test.ts b/packages/server/src/server/workspace-directory.test.ts index ab2ead15aa..85e8aeb600 100644 --- a/packages/server/src/server/workspace-directory.test.ts +++ b/packages/server/src/server/workspace-directory.test.ts @@ -343,6 +343,21 @@ describe("WorkspaceDirectory", () => { await expect(workspace.workspaceStatus()).resolves.toBe("running"); }); + test("an errored root agent shows failed only until its error is read", async () => { + const unread = new WorkspaceStatus(); + unread.hasRootAgent({ + id: "root-agent", + status: "error", + requiresAttention: true, + attentionReason: "error", + }); + await expect(unread.workspaceStatus()).resolves.toBe("failed"); + + const read = new WorkspaceStatus(); + read.hasRootAgent({ id: "root-agent", status: "error", requiresAttention: false }); + await expect(read.workspaceStatus()).resolves.toBe("done"); + }); + test("a running background agent does not make its workspace running", async () => { const workspace = new WorkspaceStatus();