diff --git a/plugins/muse-provider/server/children.ts b/plugins/muse-provider/server/children.ts index 69e0f4a855..7b8e11632e 100644 --- a/plugins/muse-provider/server/children.ts +++ b/plugins/muse-provider/server/children.ts @@ -21,6 +21,7 @@ export class Children { private readonly parentId: string, private readonly cwd: string, private readonly emit: (event: ProviderEvent) => void, + private readonly subsessions: boolean, ) {} async update(item: WireItem): Promise { @@ -34,6 +35,10 @@ export class Children { ) ) return; + // Emitting a child session.opened with parentSessionId is only valid when the + // parent session negotiated the session.subsession capability; the daemon + // fails the provider connection otherwise. + if (!this.subsessions) return; const id = item.childSessionId; if (!this.children.has(id)) { const { session } = await this.host.request( diff --git a/plugins/muse-provider/server/provider.ts b/plugins/muse-provider/server/provider.ts index 8d9a3ddfea..ce2518cbc2 100644 --- a/plugins/muse-provider/server/provider.ts +++ b/plugins/muse-provider/server/provider.ts @@ -27,6 +27,7 @@ const capabilities = [ "session.persistence", "permission", "session.list", + "session.subsession", ] as const; export function createMuseProvider(usage: Usage): ProviderRegistration { diff --git a/plugins/muse-provider/server/session.ts b/plugins/muse-provider/server/session.ts index 31066c5b04..6a5a690102 100644 --- a/plugins/muse-provider/server/session.ts +++ b/plugins/muse-provider/server/session.ts @@ -142,7 +142,13 @@ export class Session { const nativeId = response.session.sessionId; this.liveState = { nativeId, - children: new Children(this.host, this.options.id, this.config.cwd, this.options.emit), + children: new Children( + this.host, + this.options.id, + this.config.cwd, + this.options.emit, + this.options.capabilities.includes("session.subsession"), + ), timeline: new Timeline( this.host, nativeId, diff --git a/plugins/muse-provider/test/provider.test.ts b/plugins/muse-provider/test/provider.test.ts index 4868634d70..dae8029245 100644 --- a/plugins/muse-provider/test/provider.test.ts +++ b/plugins/muse-provider/test/provider.test.ts @@ -45,6 +45,7 @@ async function harness( scenario = "text-reasoning", env: Record = {}, providerOptions?: Record, + connectCapabilities: readonly string[] = PROVIDER_CAPABILITIES, ) { const root = await mkdtemp(path.join(os.tmpdir(), "muse-provider-test-")); roots.push(root); @@ -62,7 +63,7 @@ async function harness( const connection = await provider.connect({ launch, versions: [1], - capabilities: PROVIDER_CAPABILITIES, + capabilities: connectCapabilities, }); connections.push(connection); const events: ProviderEvent[] = []; @@ -1192,6 +1193,20 @@ for (const variant of ["tool", "dedicated", "workflow"]) { ).toHaveLength(1); }); } + +test("subagent children are not projected when session.subsession is not negotiated", async () => { + const h = await harness( + "subagent", + {}, + undefined, + PROVIDER_CAPABILITIES.filter((capability) => capability !== "session.subsession"), + ); + await h.open(); + await h.prompt(); + await h.wait((e) => e.type === "session.turn" && e.state === "completed"); + expect(h.events.filter((e) => e.type === "session.opened" && e.parentSessionId)).toHaveLength(0); + expect((await h.recorded()).filter((f) => f.method === "session/read")).toEqual([]); +}); test("gap backfill repairs a missing completion without duplicating item revisions", async () => { const h = await harness("text-reasoning", { MUSE_TEST_GAP: "1" }); await h.open();