From 12be4c0a064dcb962fc275290258dffbca021e9c Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Thu, 17 Sep 2026 09:38:02 +0300 Subject: [PATCH] fix(coding-agent): close the RLM progress-note and collect clusters - a run without a publication or progress notes no longer throws on cancel/snapshot, which is what mock and restored runs look like - the registry extras only carry fields the snapshot actually has, so in-memory equality and the kernel's roster view agree - the progress-note and collect suites mirror the fork's contract: the kernel discovers models before spawning and names the model it wants - the recursion suite isolates RLM_DEPTH / RLM_MAX_DEPTH, which leak in from a parent kernel when the tests run inside one --- .../coding-agent/src/core/agent-session.ts | 51 +++++++++++-------- .../test/agent-session-recursion.test.ts | 46 +++++++++++++++++ .../coding-agent/test/rlm-collect.test.ts | 7 ++- .../test/rlm-progress-notes.test.ts | 7 ++- 4 files changed, 89 insertions(+), 22 deletions(-) diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 5f73c74c8..5fa63ec72 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -11292,7 +11292,7 @@ export class AgentSession { run.status = "cancelled"; if (this._sessionInputPumpSuspended) this._abandonRlmRunForQuiescence(run); run.error = reason; - run.publication.reject(new Error(reason)); + run.publication?.reject(new Error(reason)); run.abort(); // Surface the cancellation immediately; the run's own terminal update is // delayed indefinitely when the child is stuck mid-stream, which is @@ -11398,24 +11398,35 @@ export class AgentSession { | "last_activity_at" | "activity_stale_ms" > { - return { - // Project to the registry's snake_case wire shape; the Python kernel - // reads activity.tool_name directly from the JSON payload. - activity: snapshot.activity - ? { - kind: snapshot.activity.kind, - ...(snapshot.activity.toolName ? { tool_name: snapshot.activity.toolName } : {}), - } - : undefined, - tool_use_count: snapshot.toolUseCount, - duration_ms: snapshot.durationMs, - answer_preview: snapshot.answerPreview?.slice(0, RLM_REGISTRY_ANSWER_PREVIEW_MAX_LENGTH), - replied_since_task: snapshot.repliedSinceTask, - progress_note: snapshot.progressNote, - label: snapshot.label.slice(0, RLM_REGISTRY_LABEL_MAX_LENGTH), - last_activity_at: snapshot.lastActivityAt, - activity_stale_ms: snapshot.activityStaleMs, - }; + const extras: Pick< + RlmSubagentRegistryEntry, + | "activity" + | "tool_use_count" + | "duration_ms" + | "answer_preview" + | "replied_since_task" + | "progress_note" + | "label" + | "last_activity_at" + | "activity_stale_ms" + > = {}; + if (snapshot.activity) { + extras.activity = { + kind: snapshot.activity.kind, + ...(snapshot.activity.toolName ? { tool_name: snapshot.activity.toolName } : {}), + }; + } + if (snapshot.toolUseCount !== undefined) extras.tool_use_count = snapshot.toolUseCount; + if (snapshot.durationMs !== undefined) extras.duration_ms = snapshot.durationMs; + if (snapshot.answerPreview !== undefined) { + extras.answer_preview = snapshot.answerPreview.slice(0, RLM_REGISTRY_ANSWER_PREVIEW_MAX_LENGTH); + } + if (snapshot.repliedSinceTask !== undefined) extras.replied_since_task = snapshot.repliedSinceTask; + if (snapshot.progressNote !== undefined) extras.progress_note = snapshot.progressNote; + if (snapshot.label !== undefined) extras.label = snapshot.label.slice(0, RLM_REGISTRY_LABEL_MAX_LENGTH); + if (snapshot.lastActivityAt !== undefined) extras.last_activity_at = snapshot.lastActivityAt; + if (snapshot.activityStaleMs !== undefined) extras.activity_stale_ms = snapshot.activityStaleMs; + return extras; } private _buildRlmSubagentList(listedAgents?: AgentSessionMessageListResult): RlmListSubagentsResult { @@ -11996,7 +12007,7 @@ export class AgentSession { sessionDir: run.sessionDir, activity: run.activity, repliedSinceTask: child?._repliedToParentSinceTask, - progressNote: run.progressNotes.at(-1), + progressNote: run.progressNotes?.at(-1), lastActivityAt: run.lastActivityAt, activityStaleMs: rlmActivityStaleMs(run.status, run.activity, run.lastActivityAt, run.lastActivityMonotonicAt), error: run.error, diff --git a/packages/coding-agent/test/agent-session-recursion.test.ts b/packages/coding-agent/test/agent-session-recursion.test.ts index c90ba4b01..46c008a11 100644 --- a/packages/coding-agent/test/agent-session-recursion.test.ts +++ b/packages/coding-agent/test/agent-session-recursion.test.ts @@ -123,6 +123,7 @@ interface InspectableRlmRun { error?: string; abandonedForQuiescence?: boolean; activity?: { kind: string }; + progressNotes?: string[]; emitUpdate?: () => void; publication?: { promise: Promise; resolve(): void; reject(error: Error): void }; settlement?: { promise: Promise; resolve(): void; reject(error: Error): void }; @@ -189,6 +190,8 @@ describe("AgentSession rlm recursion", () => { let tempDir: string; let session: AgentSession | undefined; let previousAgentDir: string | undefined; + let previousRlmDepth: string | undefined; + let previousRlmMaxDepth: string | undefined; beforeEach(() => { tempDir = join(tmpdir(), `pi-rlm-recursion-${Date.now()}-${Math.random().toString(36).slice(2)}`); @@ -198,6 +201,10 @@ describe("AgentSession rlm recursion", () => { // developer's real rlm-models.toml. previousAgentDir = process.env[ENV_AGENT_DIR]; process.env[ENV_AGENT_DIR] = tempDir; + previousRlmDepth = process.env.RLM_DEPTH; + delete process.env.RLM_DEPTH; + previousRlmMaxDepth = process.env.RLM_MAX_DEPTH; + delete process.env.RLM_MAX_DEPTH; }); afterEach(() => { @@ -209,6 +216,16 @@ describe("AgentSession rlm recursion", () => { } else { process.env[ENV_AGENT_DIR] = previousAgentDir; } + if (previousRlmDepth === undefined) { + delete process.env.RLM_DEPTH; + } else { + process.env.RLM_DEPTH = previousRlmDepth; + } + if (previousRlmMaxDepth === undefined) { + delete process.env.RLM_MAX_DEPTH; + } else { + process.env.RLM_MAX_DEPTH = previousRlmMaxDepth; + } }); function createSession( @@ -757,6 +774,7 @@ describe("AgentSession rlm recursion", () => { abort: () => {}, status: index === 1 ? "queued" : "running", settled: false, + progressNotes: [], }); if (hiding === "detached") { @@ -769,6 +787,7 @@ describe("AgentSession rlm recursion", () => { abort: () => {}, status: "cancelled", settled: false, + progressNotes: [], detachedDeletion: { rlm_child_id: id, active_session_id: null, @@ -2199,6 +2218,11 @@ describe("AgentSession rlm recursion", () => { session_name: expectedSessionName, session_dir: result.session_dir, status: "completed", + answer_preview: "child answer: retained worker", + duration_ms: expect.any(Number), + label: "retained worker", + last_activity_at: expect.any(Number), + replied_since_task: false, model: "anthropic/claude-sonnet-4-5", turns: expect.any(Number), tokens: expect.any(Number), @@ -2474,6 +2498,10 @@ describe("AgentSession rlm recursion", () => { session_name: createDefaultRlmSubagentSessionName("slow shard", rootRun.id), session_dir: rootRun.sessionDir, status: "running", + activity: { kind: "waiting" }, + label: "slow shard", + last_activity_at: expect.any(Number), + replied_since_task: false, model: "anthropic/claude-sonnet-4-5", turns: 0, tokens: 0, @@ -3508,6 +3536,7 @@ describe("AgentSession rlm recursion", () => { abort: () => {}, status: "running", settled: false, + progressNotes: [], }); const root = createSession(); const rootInternals = root as unknown as InspectableRlmSession; @@ -3520,6 +3549,7 @@ describe("AgentSession rlm recursion", () => { abort: () => {}, status: "running", settled: false, + progressNotes: [], publication: deferred(), settlement: deferred(), session: child, @@ -4800,16 +4830,32 @@ interface InspectableRlmDirSession { describe("AgentSession RLM session dir", () => { let tempDir: string; let session: AgentSession | undefined; + let previousRlmDepth: string | undefined; + let previousRlmMaxDepth: string | undefined; beforeEach(() => { tempDir = join(tmpdir(), `pi-rlm-dir-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(tempDir, { recursive: true }); + previousRlmDepth = process.env.RLM_DEPTH; + delete process.env.RLM_DEPTH; + previousRlmMaxDepth = process.env.RLM_MAX_DEPTH; + delete process.env.RLM_MAX_DEPTH; }); afterEach(() => { session?.dispose(); session = undefined; rmSync(tempDir, { recursive: true, force: true }); + if (previousRlmDepth === undefined) { + delete process.env.RLM_DEPTH; + } else { + process.env.RLM_DEPTH = previousRlmDepth; + } + if (previousRlmMaxDepth === undefined) { + delete process.env.RLM_MAX_DEPTH; + } else { + process.env.RLM_MAX_DEPTH = previousRlmMaxDepth; + } }); function createSession( diff --git a/packages/coding-agent/test/rlm-collect.test.ts b/packages/coding-agent/test/rlm-collect.test.ts index 1564e56bb..71935dc7a 100644 --- a/packages/coding-agent/test/rlm-collect.test.ts +++ b/packages/coding-agent/test/rlm-collect.test.ts @@ -85,7 +85,7 @@ describe("rlm.collect typed fan-in", () => { initialState: { model, systemPrompt: "", tools: [], thinkingLevel: "off" }, streamFn: (_model, context) => streamAnswer(`child answer: ${userText(context)}`), }); - return new AgentSession({ + const s = new AgentSession({ agent, sessionManager: SessionManager.create(tempDir, join(tempDir, "sessions")), settingsManager: SettingsManager.create(tempDir, tempDir), @@ -93,6 +93,11 @@ describe("rlm.collect typed fan-in", () => { modelRegistry: ModelRegistry.create(authStorage, join(tempDir, "models.json")), resourceLoader: createTestResourceLoader(), }); + s.hasCalledRlmModels = true; + const originalRunRlmChild = s.runRlmChild.bind(s); + s.runRlmChild = (prompt, kwargs = {}, spawnCode) => + originalRunRlmChild(prompt, { model: `${model.provider}/${model.id}`, ...kwargs }, spawnCode); + return s; } it("returns a typed envelope once the child settles", async () => { diff --git a/packages/coding-agent/test/rlm-progress-notes.test.ts b/packages/coding-agent/test/rlm-progress-notes.test.ts index 737b6694a..2a9813cbe 100644 --- a/packages/coding-agent/test/rlm-progress-notes.test.ts +++ b/packages/coding-agent/test/rlm-progress-notes.test.ts @@ -146,7 +146,7 @@ describe("rlm.progress.note child progress channel", () => { return stream; }), }); - return new AgentSession({ + const s = new AgentSession({ agent, sessionManager: SessionManager.create(tempDir, sessionsDir), settingsManager: SettingsManager.create(tempDir, tempDir), @@ -154,6 +154,11 @@ describe("rlm.progress.note child progress channel", () => { modelRegistry: ModelRegistry.create(authStorage, join(tempDir, "models.json")), resourceLoader: createTestResourceLoader(), }); + s.hasCalledRlmModels = true; + const originalRunRlmChild = s.runRlmChild.bind(s); + s.runRlmChild = (prompt, kwargs = {}, spawnCode) => + originalRunRlmChild(prompt, { model: `${model.provider}/${model.id}`, ...kwargs }, spawnCode); + return s; } it("validates host payload shape", async () => { -- 2.51.2