From f9de799cb5e9afe221df255b40b4df8336ef60fe Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Tue, 15 Sep 2026 19:13:05 +0300 Subject: [PATCH] fix(coding-agent): back out three overreaches the wide regression sweep caught The broad sweep (agent-session-recursion + daemon-mode, 341 tests) found 35 failures, all from this session's changes. Three fixes, one honest revert: 1. R9's daemon change is REVERTED. Forwarding the whole worker environment to a session it creates leaks unrelated provider keys into a session that was never granted them, and it broke seven daemon tests that pin the deliberate narrow launch env. The worker's PATH already comes from the supervisor's seeded client environment, so the audited `cargo: command not found` case is covered by the runtime half (R9's `_child_env` inheritance and PATH floor) plus the launch env the client sends. A daemon started by launchd with no client environment has no developer environment to inherit, and that is now stated rather than papered over. 2. R10's backpressure wait treated a session that cannot report a queue depth (a stub, or a passive child) as saturated and waited the full 30s, which timed out three daemon messaging tests. An unknown depth is now "not saturated". 3. R11's obligation sentence is REMOVED from the task envelope: it leaked into every echo of the task (child answer previews included) and the doctrine in the child's system prompt already carries the same rule. The harvest itself stays, and the two tests that pinned the old plain no-reply notice now pin the harvested shape. Sweep after the fixes: agent-session-recursion + daemon-mode = 340 passed, 1 failed, and that one is the known environment case (`RLM_MAX_DEPTH=2` exported, so `source: env` instead of `default`; it passes with the variable unset). --- packages/coding-agent/src/core/agent-session.ts | 7 +++---- packages/coding-agent/src/core/prompts/index.ts | 1 - packages/coding-agent/src/core/prompts/rlm.ts | 12 ------------ .../coding-agent/src/modes/daemon/daemon-mode.ts | 16 +++++++++------- .../test/agent-session-recursion.test.ts | 4 +++- .../test/suite/subagent-reply-harvesting.test.ts | 9 ++------- 6 files changed, 17 insertions(+), 32 deletions(-) diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index e3aaaa359..6eb6caa0e 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -214,7 +214,6 @@ import { findExactModelReferenceMatch } from "./model-resolver.js"; import { throwIfPromptAdmissionCancelled } from "./prompt-admission.js"; import { evaluateActiveCacheTtl, extractSessionCacheState, type PromptCacheSnapshot } from "./prompt-cache.js"; import { expandPromptTemplate, type PromptTemplate, parseCommandArgs } from "./prompt-templates.js"; -import { buildRlmChildTaskEnvelope } from "./prompts/index.js"; import { isAgentLifecycleFailure, isFauxProviderQueueExhausted, @@ -12464,9 +12463,9 @@ export class AgentSession { } }); run.unsubscribe = unsubscribeChildEvents; - // R11: the reply obligation rides the task envelope, so a child cannot miss it - // the way it can miss a line buried in the system prompt. - const content = buildRlmChildTaskEnvelope(prompt); + // The task envelope stays exactly the task: the reply obligation lives in the + // child doctrine (system prompt), so an echo of the prompt is still the task. + const content = `[task from parent]\n\n${prompt}`; const spawnMessage: AgentSessionMessage = { role: "custom", customType: AGENT_MESSAGE_CUSTOM_TYPE, diff --git a/packages/coding-agent/src/core/prompts/index.ts b/packages/coding-agent/src/core/prompts/index.ts index 251f7f69b..11bcbf732 100644 --- a/packages/coding-agent/src/core/prompts/index.ts +++ b/packages/coding-agent/src/core/prompts/index.ts @@ -10,7 +10,6 @@ export { } from "./payload-ceiling.js"; export { buildChildAgentDoctrine, - buildRlmChildTaskEnvelope, buildRlmPrompt, buildSubagentGuidance, type ChildAgentDoctrineOptions, diff --git a/packages/coding-agent/src/core/prompts/rlm.ts b/packages/coding-agent/src/core/prompts/rlm.ts index 48a2edf5a..e7b23b6ee 100644 --- a/packages/coding-agent/src/core/prompts/rlm.ts +++ b/packages/coding-agent/src/core/prompts/rlm.ts @@ -66,18 +66,6 @@ export interface ChildAgentDoctrineOptions { activeTools?: string[]; } -/** - * The envelope a parent's task arrives in. The reply obligation rides here rather than only - * in the system prompt, because a child cannot miss the message that carries its task. - */ -export function buildRlmChildTaskEnvelope(prompt: string): string { - return `[task from parent] - -${prompt} - -When this task is done, send its result to your parent with \`await agent_message.send(result, receiver_role="parent")\`. Finishing without a reply forwards your last assistant text to the parent, tagged as unreviewed.`; -} - export function buildChildAgentDoctrine(options: ChildAgentDoctrineOptions): string | undefined { const depth = options.depth ?? 0; const hasIpython = options.activeTools === undefined || options.activeTools.includes("ipython"); diff --git a/packages/coding-agent/src/modes/daemon/daemon-mode.ts b/packages/coding-agent/src/modes/daemon/daemon-mode.ts index b171aee3f..25a947c68 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-mode.ts @@ -2825,12 +2825,11 @@ export class AgentDaemon { (runtimeConfig?.provider ?? parentState.runtime.session.model?.provider) === options.model.provider ? runtimeConfig?.apiKey : undefined; - // The worker already carries the launching shell's environment (the supervisor - // seeds it with the client's launchEnv). Forwarding PATH alone starved the child - // session of CARGO_HOME/RUSTUP_HOME/NIX_*/SSH_AUTH_SOCK, which is how a swarm - // died with exit 127 `cargo: command not found` under a parent shell that had - // the toolchain. - const launchEnv = collectDaemonLaunchEnv(); + // Deliberately narrow: PATH comes from the worker, which the supervisor seeded with + // the launching client's environment, plus the one provider key this session needs. + // Forwarding the whole environment here would leak unrelated provider keys into a + // session that was never granted them. + const launchEnv = collectDaemonLaunchEnv({ PATH: process.env.PATH }); const envKey = authSource === "environment" ? findEnvKeys(options.model.provider)?.[0] : undefined; if (envKey && process.env[envKey]) launchEnv[envKey] = process.env[envKey]; if (options.model.provider === "prime-inference" && process.env.PRIME_TEAM_ID !== undefined) { @@ -6594,7 +6593,10 @@ export class AgentDaemon { private async waitForAgentMessageCapacity(targetState: ActiveSessionState): Promise { const deadline = Date.now() + AGENT_MESSAGE_BACKPRESSURE_WAIT_MS; while (Date.now() < deadline && !this.shuttingDown) { - if (targetState.runtime.session.unfinishedActionCount < DEFAULT_AGENT_MESSAGE_MAX_PENDING_PER_SESSION) return; + // A session that cannot report a depth (a stub, a passive child) is never treated as + // saturated: waiting on an unknown would stall delivery for the whole window. + const unfinished = targetState.runtime.session.unfinishedActionCount ?? 0; + if (!(unfinished >= DEFAULT_AGENT_MESSAGE_MAX_PENDING_PER_SESSION)) return; await new Promise((resolveDelay) => setTimeout(resolveDelay, 250)); } } diff --git a/packages/coding-agent/test/agent-session-recursion.test.ts b/packages/coding-agent/test/agent-session-recursion.test.ts index 176be843a..a0f157825 100644 --- a/packages/coding-agent/test/agent-session-recursion.test.ts +++ b/packages/coding-agent/test/agent-session-recursion.test.ts @@ -1428,7 +1428,7 @@ describe("AgentSession rlm recursion", () => { ])("injects exactly one $label notice for a child run", async ({ cancel, expected }) => { const { root, releaseChild, hasStarted } = createGatedRoot(); - it("injects exactly one notice with a preview when a child completes without replying", async () => { + it("harvests a silent child's last assistant text instead of only reporting the silence", async () => { const root = createSession(); await spawnChild(root, "silent child", { name: "silent-worker" }); @@ -1437,6 +1437,8 @@ describe("AgentSession rlm recursion", () => { (message) => message.role === "custom" && message.customType === "rlm_child_terminal_notice", ); expect(notices).toHaveLength(1); + // R11: a child that produced text hands it over, tagged, rather than making the + // parent wait for a reply that will never come. expect(notices[0]).toMatchObject({ content: "[child-exited: no-reply child:silent-worker]\n\nLast assistant text: child answer: silent child", details: { diff --git a/packages/coding-agent/test/suite/subagent-reply-harvesting.test.ts b/packages/coding-agent/test/suite/subagent-reply-harvesting.test.ts index 89ad2b215..ed315e405 100644 --- a/packages/coding-agent/test/suite/subagent-reply-harvesting.test.ts +++ b/packages/coding-agent/test/suite/subagent-reply-harvesting.test.ts @@ -1,7 +1,7 @@ import { type FauxModelDefinition, fauxAssistantMessage } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; import type { CustomMessage } from "../../src/core/messages.js"; -import { buildChildAgentDoctrine, buildRlmChildTaskEnvelope } from "../../src/core/prompts/index.js"; +import { buildChildAgentDoctrine } from "../../src/core/prompts/index.js"; import { createHarness, type Harness } from "./harness.js"; const PARENT_MODELS: FauxModelDefinition[] = [{ id: "faux-parent", reasoning: true }]; @@ -78,12 +78,7 @@ describe("subagent reply harvesting", () => { expect(notices(parent!, "harvested_fallback")).toHaveLength(0); }); - it("states the reply obligation in the envelope that carries the task", () => { - const envelope = buildRlmChildTaskEnvelope("do the work"); - expect(envelope.startsWith("[task from parent]\n\ndo the work")).toBe(true); - expect(envelope).toContain('receiver_role="parent"'); - expect(envelope).toContain("tagged as unreviewed"); - + it("states the reply obligation in the child doctrine, not in the bare task", () => { const doctrine = buildChildAgentDoctrine({ depth: 1, installedSkills: ["agent_message", "ipython"], -- 2.54.0