Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0f66949
feat(coding-agent): event-driven supervisor agent roster; serve list …
snimu Aug 29, 2026
1573af1
fix(coding-agent): review fixes for the supervisor agent roster
snimu Aug 29, 2026
1f5c3d2
fix(coding-agent): roster review fixes round two
snimu Aug 29, 2026
62cb7b9
fix(coding-agent): roster bot-review fixes
snimu Aug 29, 2026
1bd50df
fix(coding-agent): roster bot-review fixes round two
snimu Aug 29, 2026
e7ca988
fix(coding-agent): child deletes never proceed past an unreadable spa…
snimu Aug 29, 2026
d98a28f
fix(coding-agent): classify unreadable delete targets through the spa…
snimu Aug 29, 2026
7f7dd96
fix(coding-agent): roster bot-review fixes round three
snimu Aug 29, 2026
a43fdbb
fix(coding-agent): roster bot-review fixes round four
snimu Aug 29, 2026
ad7cdd4
refactor(coding-agent): drop the lossless roster channel; disk is dur…
snimu Aug 29, 2026
c6176b1
perf(coding-agent): cache roster row serializations across flushes
snimu Aug 29, 2026
24ca211
refactor(coding-agent): restart pre-roster workers on adoption; drop …
snimu Aug 29, 2026
8cf2cbf
fix(coding-agent): roster rework review fixes, supervisor and worker …
snimu Aug 29, 2026
0082257
test(coding-agent): await owned process exits before teardown rmSync
snimu Aug 29, 2026
233b958
fix(coding-agent): serialize roster pulls with frame applies and hard…
snimu Aug 29, 2026
8545c20
test(coding-agent): pin snapshot/pull serialization, pre-roster resta…
snimu Aug 29, 2026
993b884
fix(coding-agent): abort queued roster applies for unregistered worke…
snimu Aug 29, 2026
9ef24b4
test(coding-agent): let the stop land mid pre-read in the snapshot-ab…
snimu Aug 29, 2026
3163dae
fix(coding-agent): single-flight roster repair pull with a logged fai…
snimu Aug 29, 2026
c3eb977
test(coding-agent): reduce the roster suite to distinct behavior pins
snimu Aug 30, 2026
c4bcbca
test(coding-agent): drop an unused import after the projection pin re…
snimu Aug 30, 2026
603e044
test(coding-agent): fix formatting after the removal-id pin cut
snimu Aug 30, 2026
4465a37
test(coding-agent): final reviewer-directed roster suite cuts
snimu Aug 30, 2026
1257659
test(coding-agent): biome format for the population matrix
snimu Aug 30, 2026
f56e5ba
test(coding-agent): pin resident and seeded rows side by side in one …
snimu Aug 30, 2026
c88236a
chore(coding-agent): comment sweep — one-line present-tense rationale…
snimu Aug 30, 2026
b262931
fix(coding-agent): republish retry/tool transitions and guard pulled …
snimu Aug 30, 2026
ec6cb7d
refactor(coding-agent): rename AgentRosterLedger to AgentRoster
snimu Aug 30, 2026
afafbfa
refactor(coding-agent): one owner each for busy/status adapters, regi…
snimu Aug 31, 2026
544b11e
fix(coding-agent): supervisor roster correctness batch
snimu Aug 31, 2026
bfcb78e
test(coding-agent): pin the roster correctness batch
snimu Aug 31, 2026
02381f7
fix(coding-agent): let composed session rows beat lingering queued ma…
snimu Aug 31, 2026
6fb3c6a
fix(coding-agent): keep unserved worker files listed as inactive rows…
snimu Aug 31, 2026
d80d7b3
fix(coding-agent): guard roster applies against dead registrations an…
snimu Aug 31, 2026
05fb070
fix(coding-agent): drop client-owned workers' roster rows on unregist…
snimu Aug 31, 2026
5fa2b95
fix(coding-agent): re-verify pid identity at the last moment before t…
snimu Aug 31, 2026
642963e
fix(coding-agent): import the moved busy predicate for the empty-sess…
snimu Aug 31, 2026
a1766fa
fix(coding-agent): serve empty-detach eviction from the roster with w…
snimu Aug 31, 2026
b185223
fix(coding-agent): flush the roster on plain cron job add and cancel
snimu Aug 31, 2026
6c66b5f
fix(coding-agent): keep the hydrated summary when a snapshot reseeds …
snimu Aug 31, 2026
634a19d
chore(coding-agent): roster review nits
snimu Aug 31, 2026
4bff2b4
fix(coding-agent): scope pending roster removals to one incarnation a…
snimu Aug 31, 2026
2ee3fc8
chore(coding-agent): slim roster comments and consolidate roster tests
snimu Aug 31, 2026
1a6b9c1
fix(coding-agent): roster identity and staleness fixes from the sixth…
snimu Aug 31, 2026
afc7623
chore(coding-agent): second slim pass on roster tests and comments
snimu Aug 31, 2026
5dc7674
fix(coding-agent): seventh review round — spawn-append scoping, stat-…
snimu Aug 31, 2026
0acfeb2
fix(coding-agent): remove, not passivate, rows renamed by in-place se…
snimu Aug 31, 2026
385f566
fix(coding-agent): ninth review round — one family snapshot, family-s…
snimu Sep 1, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Made the daemon supervisor own an event-driven agent roster: workers push roster deltas on session events and `list` is served from the supervisor's ledger with zero worker round-trips. Rows are as fresh as the owning worker's last delta; a silent worker's rows are annotated (recovering, last-heard-from) rather than dropped, and the surfaces that display those annotations ship in the follow-up PR.
- Tracked admitted subagent runs in the supervisor roster from the moment they are queued (they appear in `list` once their session exists), and kept passivated or evicted agents listed as inactive rows instead of disappearing (client-owned workers stay private: their rows are dropped when the worker goes away).
- Tracked worker liveness in the supervisor roster: a dead worker's rows are flagged "recovering" the moment its socket closes, and rows of silent workers carry a last-heard-from time. These fields are supervisor-internal here; the roster surfaces that display them ship in the follow-up PR.
186 changes: 186 additions & 0 deletions packages/coding-agent/src/modes/daemon/agent-roster.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { canonicalSessionPath } from "../../core/session-lease.js";
import type { SessionSummary } from "./daemon-session-list.js";

// One status formula shared by every agent surface; surfaces adapt their inputs and never reimplement it.
export type AgentRosterStatus = "running" | "idle" | "inactive";

Expand All @@ -16,3 +19,186 @@ export function classifyAgentStatus(input: AgentStatusInput): AgentRosterStatus
if (!input.resident) return "inactive";
return input.busy || input.hasActiveHeartbeat ? "running" : "idle";
}

export function isSessionSummaryBusy(
summary: Pick<SessionSummary, "isSessionActive" | "hasRunningRlmChildren">,
): boolean {
return summary.isSessionActive || summary.hasRunningRlmChildren === true;
}

export function classifySessionRosterStatus(
summary: Pick<
SessionSummary,
"activeSessionId" | "activity" | "isSessionActive" | "hasRunningRlmChildren" | "hasActiveHeartbeat"
>,
queuedChild = false,
): AgentRosterStatus {
return classifyAgentStatus({
resident: !!summary.activeSessionId,
queuedChild,
busy: summary.activity === "working" || isSessionSummaryBusy(summary),
hasActiveHeartbeat: summary.hasActiveHeartbeat === true,
});
}

export type RosterSessionSummary = Omit<SessionSummary, "streamingMessage" | "sessionActions" | "diagnostics">;

export interface WorkerRosterEntry {
agentId: string;
queuedChild?: true;
seededCwd?: true;
summary: RosterSessionSummary;
}

export interface AgentRosterEntry extends WorkerRosterEntry {
status: AgentRosterStatus;
statusLabel?: "queued" | "recovering" | "failed";
lastHeardFromAt?: string;
workerId?: string;
}

// Child ids are only unique per parent (32-bit, mkdir-checked); the parent path qualifies them daemon-wide.
export function rosterAgentIdForSummary(
summary: Pick<
SessionSummary,
"runtimeKind" | "rlmChildId" | "sessionId" | "parentSessionPath" | "parentActiveSessionId"
>,
): string {
if (summary.runtimeKind === "subagent" && summary.rlmChildId) {
// No-session parents have no path (and no ledger edge); their live parent id still disambiguates.
const parentKey = summary.parentSessionPath
? canonicalSessionPath(summary.parentSessionPath)
: summary.parentActiveSessionId;
return parentKey ? `${parentKey}#${summary.rlmChildId}` : summary.rlmChildId;
}
return summary.sessionId;
}

export function workerRosterEntryFromSummary(summary: SessionSummary): WorkerRosterEntry {
const { streamingMessage, sessionActions, diagnostics, ...slim } = summary;
return { agentId: rosterAgentIdForSummary(summary), summary: slim };
}

function classifyWorkerRosterEntry(entry: WorkerRosterEntry): AgentRosterStatus {
return classifySessionRosterStatus(entry.summary, entry.queuedChild === true);
}
Comment thread
cursor[bot] marked this conversation as resolved.

export function passivatedWorkerRosterEntry(
entry: WorkerRosterEntry,
registrations?: { hasRegisteredHeartbeat: boolean; hasRegisteredCronJob: boolean },
): WorkerRosterEntry {
const {
activeSessionId,
hasActiveHeartbeat,
hasRegisteredHeartbeat,
hasRegisteredCronJob,
hasRunningRlmChildren,
isBashRunning,
isRunningTools,
workerState,
workerPid,
...summary
} = entry.summary;
return {
agentId: entry.agentId,
summary: {
...summary,
id: summary.sessionId,
activity: "idle",
isSessionActive: false,
isStreaming: false,
isCompacting: false,
attachedClients: 0,
...(registrations?.hasRegisteredHeartbeat ? { hasRegisteredHeartbeat: true } : {}),
...(registrations?.hasRegisteredCronJob ? { hasRegisteredCronJob: true } : {}),
},
};
}

export function sessionSummaryFromRosterEntry(entry: WorkerRosterEntry): SessionSummary {
return { ...entry.summary, sessionActions: { queuedCount: 0, steering: [], followUps: [] } };
}

export class AgentRoster {
private readonly entries = new Map<string, AgentRosterEntry>();
private readonly agentIdByActiveSessionId = new Map<string, string>();
private readonly agentIdBySessionFile = new Map<string, string>();

constructor(private readonly canonicalPath: (path: string) => string) {}

values(): IterableIterator<AgentRosterEntry> {
return this.entries.values();
}

get(agentId: string): AgentRosterEntry | undefined {
return this.entries.get(agentId);
}

has(agentId: string): boolean {
return this.entries.has(agentId);
}

byActiveSessionId(activeSessionId: string): AgentRosterEntry | undefined {
const agentId = this.agentIdByActiveSessionId.get(activeSessionId);
return agentId !== undefined ? this.entries.get(agentId) : undefined;
}

bySessionFile(canonicalPath: string): AgentRosterEntry | undefined {
const agentId = this.agentIdBySessionFile.get(canonicalPath);
return agentId !== undefined ? this.entries.get(agentId) : undefined;
}

hasSessionFile(canonicalPath: string): boolean {
return this.agentIdBySessionFile.has(canonicalPath);
}

entriesForWorker(workerId: string): AgentRosterEntry[] {
return [...this.entries.values()].filter((entry) => entry.workerId === workerId);
}

write(entry: WorkerRosterEntry, workerId?: string, statusLabel?: AgentRosterEntry["statusLabel"]): AgentRosterEntry {
const stored: AgentRosterEntry = {
...entry,
status: classifyWorkerRosterEntry(entry),
...(entry.queuedChild ? { statusLabel: "queued" as const } : statusLabel ? { statusLabel } : {}),
...(workerId !== undefined ? { workerId } : {}),
};
const previous = this.entries.get(entry.agentId);
if (previous) this.dropIndexes(previous);
if (stored.summary.sessionFile) {
const file = this.canonicalPath(stored.summary.sessionFile);
const existingAgentId = this.agentIdBySessionFile.get(file);
if (existingAgentId !== undefined && existingAgentId !== entry.agentId) {
this.delete(existingAgentId);
}
this.agentIdBySessionFile.set(file, entry.agentId);
}
if (stored.summary.activeSessionId) {
this.agentIdByActiveSessionId.set(stored.summary.activeSessionId, entry.agentId);
}
this.entries.set(entry.agentId, stored);
return stored;
}

delete(agentId: string): void {
const entry = this.entries.get(agentId);
if (!entry) return;
this.dropIndexes(entry);
this.entries.delete(agentId);
}

private dropIndexes(entry: AgentRosterEntry): void {
if (
entry.summary.activeSessionId &&
this.agentIdByActiveSessionId.get(entry.summary.activeSessionId) === entry.agentId
) {
this.agentIdByActiveSessionId.delete(entry.summary.activeSessionId);
}
if (entry.summary.sessionFile) {
const file = this.canonicalPath(entry.summary.sessionFile);
if (this.agentIdBySessionFile.get(file) === entry.agentId) {
this.agentIdBySessionFile.delete(file);
}
}
}
}
Loading
Loading