diff --git a/packages/coding-agent/.changes/fix-stale-worker-status.md b/packages/coding-agent/.changes/fix-stale-worker-status.md new file mode 100644 index 0000000000..8ef9874ae1 --- /dev/null +++ b/packages/coding-agent/.changes/fix-stale-worker-status.md @@ -0,0 +1 @@ +- Fixed completed resident subagents appearing active when their in-memory execution state was stale. diff --git a/packages/coding-agent/src/modes/daemon/daemon-mode.ts b/packages/coding-agent/src/modes/daemon/daemon-mode.ts index 0bda7e5ea5..571d0ce410 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-mode.ts @@ -200,6 +200,7 @@ import { RlmSpawnLedger, } from "./rlm-ledger.js"; import { + type RlmSubagentDisplayEntry, readRlmSubagentDisplayEntry, rlmSubagentDisplayPath, writeRlmSubagentDisplayEntry, @@ -3160,13 +3161,15 @@ export class AgentDaemon { } private async createAgentObserveListResult(currentState: ActiveSessionState): Promise { - const agents = this.listTargetableSessionStates(currentState) - .filter( - (state) => - state.activeSessionId === currentState.activeSessionId || - this.isAgentFamilyReachable(currentState, state), - ) - .map((state) => this.createAgentObserveSummary(state, currentState)); + const targetableStates = this.listTargetableSessionStates(currentState).filter( + (state) => + state.activeSessionId === currentState.activeSessionId || this.isAgentFamilyReachable(currentState, state), + ); + const agents = await Promise.all( + targetableStates.map(async (state) => + this.createAgentObserveSummary(state, currentState, await this.readResidentRlmLifecycleStatus(state)), + ), + ); const residentIds = new Set(agents.map((agent) => agent.activeSessionId)); for (const passive of await this.listPassiveRlmSubagents()) { if (residentIds.has(passive.info.id)) continue; @@ -3201,7 +3204,9 @@ export class AgentDaemon { residentIds.add(passive.info.id); } return { - current: this.createAgentObserveSummary(currentState, currentState), + current: + agents.find((agent) => agent.activeSessionId === currentState.activeSessionId) ?? + this.createAgentObserveSummary(currentState, currentState), agents, }; } @@ -3213,7 +3218,11 @@ export class AgentDaemon { const targetState = await this.getOrHydrateAuthorizedAgentFamilyTarget(currentState, target); this.assertAgentFamilyReachable(currentState, targetState); return { - agent: this.createAgentObserveSummary(targetState, currentState), + agent: this.createAgentObserveSummary( + targetState, + currentState, + await this.readResidentRlmLifecycleStatus(targetState), + ), }; } @@ -3228,7 +3237,11 @@ export class AgentDaemon { const messages = targetState.runtime.session.messages; const startIndex = Math.max(0, messages.length - limit); return { - agent: this.createAgentObserveSummary(targetState, currentState), + agent: this.createAgentObserveSummary( + targetState, + currentState, + await this.readResidentRlmLifecycleStatus(targetState), + ), messages: messages .slice(startIndex) .map((message, offset) => createAgentObserveMessagePreview(message, startIndex + offset, maxChars)), @@ -3241,22 +3254,26 @@ export class AgentDaemon { private createAgentObserveSummary( state: ActiveSessionState, currentState: ActiveSessionState, + rlmLifecycleStatus?: RlmSubagentDisplayEntry["status"], ): AgentObserveAgentSummary { const summary = summaryForActiveSession(state); const session = state.runtime.session; const messages = session.messages; const latest = messages.at(-1); - const status = session.isStreaming - ? session.state.pendingToolCalls.size > 0 - ? "tool" - : "model" - : session.isCompacting - ? "compacting" - : session.isSessionActive || session.hasRunningRlmChildren() - ? "busy" - : state.clients.size > 0 - ? "user" - : "idle"; + const isTerminalRlmChild = rlmLifecycleStatus === "completed" || rlmLifecycleStatus === "deleted"; + const status = isTerminalRlmChild + ? "idle" + : session.isStreaming + ? session.state.pendingToolCalls.size > 0 + ? "tool" + : "model" + : session.isCompacting + ? "compacting" + : session.isSessionActive || session.hasRunningRlmChildren() + ? "busy" + : state.clients.size > 0 + ? "user" + : "idle"; return { activeSessionId: state.activeSessionId, sessionId: summary.sessionId, @@ -3265,12 +3282,12 @@ export class AgentDaemon { cwd: summary.cwd, status, isCurrent: state.activeSessionId === currentState.activeSessionId, - isStreaming: summary.isStreaming, - isCompacting: summary.isCompacting, + isStreaming: isTerminalRlmChild ? false : summary.isStreaming, + isCompacting: isTerminalRlmChild ? false : summary.isCompacting, attachedClients: summary.attachedClients, messageCount: summary.messageCount, - queuedCount: summary.sessionActions.queuedCount, - isSessionActive: summary.isSessionActive, + queuedCount: isTerminalRlmChild ? 0 : summary.sessionActions.queuedCount, + isSessionActive: isTerminalRlmChild ? false : summary.isSessionActive, ...(summary.parentActiveSessionId ? { parentActiveSessionId: summary.parentActiveSessionId } : {}), ...(summary.parentSessionId ? { parentSessionId: summary.parentSessionId } : {}), ...(summary.rlmChildId ? { rlmChildId: summary.rlmChildId } : {}), @@ -5338,38 +5355,56 @@ export class AgentDaemon { }; } - private createAgentMessageAgentSummary(state: ActiveSessionState): AgentSessionMessageAgentSummary { + private async readResidentRlmLifecycleStatus( + state: ActiveSessionState, + ): Promise { + const metadata = state.runtime.metadata; + if (metadata.kind !== "subagent" || !metadata.rlmChildId || !metadata.sessionDir) return undefined; + const display = await readRlmSubagentDisplayEntry(metadata.sessionDir); + return display?.childId === metadata.rlmChildId ? display.status : undefined; + } + + private createAgentMessageAgentSummary( + state: ActiveSessionState, + rlmLifecycleStatus?: RlmSubagentDisplayEntry["status"], + ): AgentSessionMessageAgentSummary { const metadata = state.runtime.metadata; const session = state.runtime.session; + const isTerminalRlmChild = rlmLifecycleStatus === "completed" || rlmLifecycleStatus === "deleted"; return { ...this.createAgentSessionMessageEndpoint(state), cwd: state.runtime.cwd, - isStreaming: session.isStreaming, - unfinishedActionCount: session.unfinishedActionCount, + isStreaming: isTerminalRlmChild ? false : session.isStreaming, + unfinishedActionCount: isTerminalRlmChild ? 0 : session.unfinishedActionCount, ...(metadata.parentActiveSessionId ? { parentActiveSessionId: metadata.parentActiveSessionId } : {}), ...(metadata.parentSessionId ? { parentSessionId: metadata.parentSessionId } : {}), ...(metadata.parentSessionFile ? { parentSessionPath: metadata.parentSessionFile } : {}), rlmDepth: session.rlmDepth, - status: classifySessionRosterStatus({ - activeSessionId: state.activeSessionId, - runtimeKind: metadata.kind, - activity: session.isSessionActive ? "working" : "idle", - isSessionActive: session.isSessionActive, - hasRunningRlmChildren: session.hasRunningRlmChildren?.() ?? false, - hasActiveHeartbeat: - this.cronStore.getHeartbeat(state.activeSessionId)?.status === "active" || - this.cronStore.listRlmHeartbeats(state.activeSessionId).some((job) => job.status === "active"), - isStreaming: session.isStreaming, - } as SessionSummary), + status: isTerminalRlmChild + ? "inactive" + : classifySessionRosterStatus({ + activeSessionId: state.activeSessionId, + runtimeKind: metadata.kind, + activity: session.isSessionActive ? "working" : "idle", + isSessionActive: session.isSessionActive, + hasRunningRlmChildren: session.hasRunningRlmChildren?.() ?? false, + hasActiveHeartbeat: + this.cronStore.getHeartbeat(state.activeSessionId)?.status === "active" || + this.cronStore.listRlmHeartbeats(state.activeSessionId).some((job) => job.status === "active"), + isStreaming: session.isStreaming, + } as SessionSummary), ...(metadata.rlmChildId ? { rlmChildId: metadata.rlmChildId } : {}), + ...(rlmLifecycleStatus ? { rlmChildRegistryStatus: rlmLifecycleStatus } : {}), ...(metadata.sessionDir ? { sessionDir: metadata.sessionDir } : {}), ...(session.sessionFile ? { sessionPath: session.sessionFile } : {}), }; } private async createAgentMessageListResult(current: ActiveSessionState): Promise { - const localAgents = this.listTargetableSessionStates(current).map((state) => - this.createAgentMessageAgentSummary(state), + const localAgents = await Promise.all( + this.listTargetableSessionStates(current).map(async (state) => + this.createAgentMessageAgentSummary(state, await this.readResidentRlmLifecycleStatus(state)), + ), ); for (const passive of await this.listPassiveRlmSubagents()) { const { entry, info } = passive; diff --git a/packages/coding-agent/test/daemon-mode.test.ts b/packages/coding-agent/test/daemon-mode.test.ts index 8f690b2642..455b05ca17 100644 --- a/packages/coding-agent/test/daemon-mode.test.ts +++ b/packages/coding-agent/test/daemon-mode.test.ts @@ -711,22 +711,37 @@ describe("daemon mode helpers", () => { metadata: { ...subagentState.runtime.metadata, rlmChildId: "child-1", + sessionDir: "/tmp/child-1", }, session: { + ...subagentState.runtime.session, sessionId: "session-child", sessionName: defaultSubagentName, - isStreaming: false, + isStreaming: true, + isSessionActive: true, + isCompacting: false, + isBashRunning: false, + hasRunningRlmChildren: () => false, + messages: [], + sessionManager: { getHeader: () => ({}), getCwd: () => "/tmp" }, + state: { pendingToolCalls: new Map() }, + unfinishedActionCount: 1, sessionActions: { queuedCount: 0, steering: [], followUps: [] }, acceptAgentMessagePrompt, }, } as never; const internals = daemon as unknown as { sessions: Map; + readResidentRlmLifecycleStatus(state: ActiveSessionState): Promise<"completed" | undefined>; createAgentMessageController( getCurrentState: () => ActiveSessionState | undefined, ): AgentSessionMessageController; }; internals.sessions.set(parentState.activeSessionId, parentState); + vi.spyOn(internals, "readResidentRlmLifecycleStatus").mockImplementation(async (state) => { + if (state === subagentState) return "completed"; + return undefined as never; + }); // A successfully completed RLM child remains idle in this daemon registry. internals.sessions.set(subagentState.activeSessionId, subagentState); @@ -739,11 +754,21 @@ describe("daemon mode helpers", () => { runtimeKind: "subagent", parentActiveSessionId: parentState.activeSessionId, rlmChildId: "child-1", + rlmChildRegistryStatus: "completed", + status: "inactive", + isStreaming: false, + unfinishedActionCount: 0, }); if (!subagentSummary?.sessionName) { throw new Error("Missing default subagent session name"); } + Object.assign(subagentState.runtime.session, { + isStreaming: false, + isSessionActive: false, + unfinishedActionCount: 0, + }); + await expect( controller.sendAgentMessage({ target: subagentSummary.sessionName,