From 5dd57fa45a6be63bebeeb9da33ee1ead578d8827 Mon Sep 17 00:00:00 2001 From: florian Date: Thu, 4 Jun 2026 14:52:37 +0200 Subject: [PATCH] fix(opencode): populate agent/model on task-tool subagent child sessions After ddc30cd15 made agent and model explicit inputs to Session.create(), the task.ts call site was never updated to pass them. The model value is already derived for the prompt invocation but sits after the create call. Child sessions ended up with NULL agent and model in the DB, breaking attribution for downstream consumers (telemetry, status-line, etc.). Hoist the parent-message fetch and model derivation above sessions.create() so the values are available at row-creation time, then pass agent: next.name and model (converted to Session.Model's { id, providerID } shape) explicitly. The prompt-invocation model keeps its own { modelID, providerID } variable. Adds regression tests (Tests A/B/C) in test/tool/task.test.ts. Fixes regression introduced by: ddc30cd15 feat(core): add session metadata support (#23068) --- packages/opencode/src/tool/task.ts | 34 +++++++++++++++--------- packages/opencode/test/tool/task.test.ts | 33 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 041a5657199c..2bd0b3d5ff3a 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -123,11 +123,33 @@ export const TaskTool = Tool.define( const parentAgent = parent.agent ? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined))) : undefined + + // Hoisted above sessions.create() so model context is available at row-creation time. + const msg = yield* MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }).pipe( + Effect.provideService(Database.Service, database), + Effect.orDie, + ) + if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message")) + const variant = msg.info.variant + + // Session.Model shape ({ id, providerID }) for sessions.create() row attribution. + // Agent config model uses { modelID }, while Session.Model uses { id } — convert here. + const sessionModel = next.model + ? { id: next.model.modelID, providerID: next.model.providerID } + : { id: msg.info.modelID, providerID: msg.info.providerID } + // Prompt-invocation shape ({ modelID, providerID }) — same as agent config model shape. + const model = { + modelID: next.model?.modelID ?? msg.info.modelID, + providerID: next.model?.providerID ?? msg.info.providerID, + } + const nextSession = session ?? (yield* sessions.create({ parentID: ctx.sessionID, title: params.description + ` (@${next.name} subagent)`, + agent: next.name, + model: sessionModel, permission: [ ...deriveSubagentSessionPermission({ parentSessionPermission: parent.permission ?? [], @@ -141,18 +163,6 @@ export const TaskTool = Tool.define( })) ?? []), ], })) - - const msg = yield* MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }).pipe( - Effect.provideService(Database.Service, database), - Effect.orDie, - ) - if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message")) - const variant = msg.info.variant - - const model = next.model ?? { - modelID: msg.info.modelID, - providerID: msg.info.providerID, - } const metadata = { parentSessionId: ctx.sessionID, sessionId: nextSession.id, diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 66ffd8658639..bc40d7f4f737 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -381,6 +381,37 @@ describe("tool.task", () => { }), ) + it.instance("execute populates agent and model on the child session row", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + + const result = yield* def.execute( + { description: "test task", prompt: "hello", subagent_type: "general" }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps: stubOps() }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + const child = yield* sessions.get(result.metadata.sessionId) + // Test A: agent must equal the dispatched subagent name + expect(child.agent).toBe("general") + // Test B: model must be populated (falls back to parent message's ref model) + expect(child.model).toBeDefined() + expect(child.model?.id).toBe(ref.modelID) + expect(child.model?.providerID).toBe(ref.providerID) + }), + ) + it.instance( "execute shapes child permissions for task, todowrite, and primary tools", () => @@ -429,6 +460,8 @@ describe("tool.task", () => { action: "allow", }, ]) + expect(child.agent).toBe("reviewer") + expect(child.model).toBeDefined() expect(seen?.tools).toEqual({ todowrite: false, bash: false,