Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Fixed new top-level daemon sessions inheriting an RLM child depth from the supervisor process.

## [0.7.3] - 2026-08-17

- Fixed assistant rendering when provider payloads contain null or sparse content blocks.
Expand Down
28 changes: 15 additions & 13 deletions packages/coding-agent/src/modes/daemon/daemon-supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2181,23 +2181,25 @@ export class DaemonSupervisor {
const orphanProcessJournalPath =
existing?.descriptor.orphanProcessJournalPath ?? join(this.descriptorDir, `${workerId}.orphans.jsonl`);
const launch = createCliSubprocessLaunchSpec(["--mode", "daemon", "--daemon-socket", socketPath]);
const workerEnvironment = createCliSubprocessEnv({
...process.env,
...launchEnv,
[DAEMON_WORKER_ROLE_ENV]: "1",
[DAEMON_WORKER_TOKEN_ENV]: token,
[DAEMON_WORKER_ACTIVE_SESSION_ID_ENV]: rootActiveSessionId,
[DAEMON_WORKER_SUPERVISOR_SOCKET_ENV]: this.socketPath,
[DAEMON_WORKER_RECOVERY_JOURNAL_ENV]: recoveryJournalPath,
[DAEMON_WORKER_STARTUP_GATE_FD_ENV]: String(WORKER_STARTUP_GATE_FD),
[ORPHAN_PROCESS_JOURNAL_ENV]: orphanProcessJournalPath,
[SESSION_LEASES_ENABLED_ENV]: "1",
[SESSION_LEASE_OWNER_ID_ENV]: rootActiveSessionId,
});
delete workerEnvironment.RLM_DEPTH;
await this.assertRecoveryAllowed();
const child: ChildProcess = spawn(launch.command, launch.args, {
cwd: createCommand.config?.cwd ?? process.cwd(),
detached: true,
env: createCliSubprocessEnv({
...process.env,
...launchEnv,
[DAEMON_WORKER_ROLE_ENV]: "1",
[DAEMON_WORKER_TOKEN_ENV]: token,
[DAEMON_WORKER_ACTIVE_SESSION_ID_ENV]: rootActiveSessionId,
[DAEMON_WORKER_SUPERVISOR_SOCKET_ENV]: this.socketPath,
[DAEMON_WORKER_RECOVERY_JOURNAL_ENV]: recoveryJournalPath,
[DAEMON_WORKER_STARTUP_GATE_FD_ENV]: String(WORKER_STARTUP_GATE_FD),
[ORPHAN_PROCESS_JOURNAL_ENV]: orphanProcessJournalPath,
[SESSION_LEASES_ENABLED_ENV]: "1",
[SESSION_LEASE_OWNER_ID_ENV]: rootActiveSessionId,
}),
env: workerEnvironment,
stdio: ["ignore", "ignore", "pipe", "pipe"],
});
const detachWorkerStderr = child.stderr
Expand Down
31 changes: 31 additions & 0 deletions packages/coding-agent/test/daemon-supervisor-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function spawnSupervisor(
socketPath: string,
cwd: string,
extraArgs: readonly string[] = [],
extraEnv: NodeJS.ProcessEnv = {},
): ChildProcess {
daemonSockets.add(socketPath);
const child = spawn(
Expand All @@ -89,6 +90,7 @@ function spawnSupervisor(
cwd,
env: {
...process.env,
...extraEnv,
[ENV_AGENT_DIR]: agentDir,
PI_OFFLINE: "1",
TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"),
Expand Down Expand Up @@ -286,6 +288,35 @@ async function startBlockingBash(client: DaemonClient, activeSessionId: string,
}

describe("daemon supervisor resident workers", () => {
it("creates top-level sessions at depth zero when the supervisor inherits a child depth", async () => {
const root = tempDir();
const agentDir = join(root, "agent");
const projectDir = join(root, "project");
const sessionDir = join(agentDir, "sessions");
const socketPath = join(tmpdir(), `prime-supervisor-depth-${process.pid}-${randomUUID().slice(0, 8)}.sock`);
mkdirSync(projectDir, { recursive: true });

const supervisor = spawnSupervisor(agentDir, socketPath, projectDir, [], { RLM_DEPTH: "1" });
const client = await connectEventually(socketPath, supervisor);
const created = await client.request({
type: "create",
config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true },
});
if (!created.success) throw new Error(created.error);
const summary = requireSummary(created.data);
if (!summary.workerPid || !summary.sessionFile) throw new Error("Worker did not expose its session identity");
workerPids.add(summary.workerPid);

expect(summary).toMatchObject({ runtimeKind: "top-level", rlmDepth: 0 });
expect(await readSessionInfo(summary.sessionFile)).toMatchObject({ rlmDepth: 0, parentSessionPath: undefined });

await client.request({ type: "shutdown" });
client.close();
await waitForProcessGone(summary.workerPid);
workerPids.delete(summary.workerPid);
await waitForSocketGone(socketPath);
}, 60_000);

it("lists, creates, and attaches passive children through their owning worker", async () => {
const root = tempDir();
const agentDir = join(root, "agent");
Expand Down