Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/coding-agent/.changes/fix-stale-worker-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed completed resident subagents appearing active when their in-memory execution state was stale.
117 changes: 76 additions & 41 deletions packages/coding-agent/src/modes/daemon/daemon-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ import {
RlmSpawnLedger,
} from "./rlm-ledger.js";
import {
type RlmSubagentDisplayEntry,
readRlmSubagentDisplayEntry,
rlmSubagentDisplayPath,
writeRlmSubagentDisplayEntry,
Expand Down Expand Up @@ -3160,13 +3161,15 @@ export class AgentDaemon {
}

private async createAgentObserveListResult(currentState: ActiveSessionState): Promise<AgentObserveListResult> {
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;
Expand Down Expand Up @@ -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,
};
}
Expand All @@ -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),
),
};
}

Expand All @@ -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)),
Expand All @@ -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,
Expand All @@ -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 } : {}),
Expand Down Expand Up @@ -5338,38 +5355,56 @@ export class AgentDaemon {
};
}

private createAgentMessageAgentSummary(state: ActiveSessionState): AgentSessionMessageAgentSummary {
private async readResidentRlmLifecycleStatus(
state: ActiveSessionState,
): Promise<RlmSubagentDisplayEntry["status"] | undefined> {
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<AgentSessionMessageListResult> {
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;
Expand Down
27 changes: 26 additions & 1 deletion packages/coding-agent/test/daemon-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ActiveSessionState>;
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);

Expand All @@ -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,
Expand Down