From 28d6501213c5bd4146a42221fd1e31f513a5c85b Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:53:37 +0200 Subject: [PATCH 1/2] fix(lineage): retain native agent effort and speed --- .../Adapters/CodexAdapterV2.test.ts | 20 +++- .../Adapters/CodexAdapterV2.ts | 103 +++++++++++++++--- .../chat/SubagentTooltipContent.tsx | 9 +- ...ThreadRelationshipsControl.agents.test.tsx | 33 ++++++ .../chat/ThreadRelationshipsControl.tsx | 2 + .../src/components/chat/V2LifecycleRow.tsx | 8 +- packages/contracts/src/orchestrationV2.ts | 1 + 7 files changed, 152 insertions(+), 24 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 602052f08ea7..ab0281b49346 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -7029,13 +7029,17 @@ describe("CodexAdapterV2 post-settle continuation", () => { : { thread: { id: name.includes("wrong child") ? "other-child" : threadId, - ...(name.startsWith("current Codex") ? { model: "gpt-6-sol" } : {}), + ...(name.startsWith("current Codex") + ? { model: "gpt-6-sol", reasoningEffort: "high" } + : {}), }, model: name.startsWith("current Codex") ? null : name === "wrong child" ? "gpt-5.6-sol" : model, + reasoningEffort: "high", + serviceTier: "priority", }, ), ); @@ -7055,6 +7059,12 @@ describe("CodexAdapterV2 post-settle continuation", () => { yield* TestClock.adjust("100 millis"); yield* harness.firstTerminal; assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model); + if (model) { + assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [ + { id: "reasoningEffort", value: "high" }, + ...(name.startsWith("current Codex") ? [] : [{ id: "serviceTier", value: "priority" }]), + ]); + } assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2); }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), ), @@ -7142,6 +7152,8 @@ describe("CodexAdapterV2 post-settle continuation", () => { threadId: RESUME_CHILD_THREAD, threadSettings: { model, + effort: "low", + serviceTier: "ultrafast", modelProvider: "openai", cwd: "/workspace", approvalPolicy: "never", @@ -7186,6 +7198,12 @@ describe("CodexAdapterV2 post-settle continuation", () => { yield* Deferred.succeed(releaseMetadata, undefined); yield* TestClock.adjust("30 seconds"); assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model); + if (method === "thread/settings/updated") { + assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [ + { id: "reasoningEffort", value: "low" }, + { id: "serviceTier", value: "ultrafast" }, + ]); + } }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), ), ); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index da0de5d48fc2..c38814020607 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1352,18 +1352,20 @@ const decodeCodexResumeMetadata = Schema.decodeUnknownEffect( Schema.Struct({ thread: Schema.Struct({ id: Schema.String, updatedAt: Schema.Number }) }), ); -const decodeCodexChildModel = Schema.decodeUnknownEffect( - Schema.Struct({ - thread: Schema.Struct({ id: Schema.String }), - model: Schema.NullOr(Schema.String), - }), -); +const CodexChildSelection = Schema.Struct({ + thread: Schema.Struct({ id: Schema.String }), + model: Schema.NullOr(Schema.String), + reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)), + serviceTier: Schema.optional(Schema.NullOr(Schema.String)), +}); +const decodeCodexChildModel = Schema.decodeUnknownEffect(CodexChildSelection); const decodeCodexChildThread = Schema.decodeUnknownEffect( Schema.Struct({ thread: Schema.Struct({ id: Schema.String, model: Schema.optional(Schema.NullOr(Schema.String)), + reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)), }), }), ); @@ -1834,7 +1836,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi const pendingRootTurns = yield* Ref.make(new Map()); const turnWaiters = yield* Ref.make(new Map>()); const subagentThreads = yield* Ref.make(new Map()); - const subagentModels = new Map(); + const subagentSelections = new Map>(); const pendingSubagentTurns = yield* Ref.make( new Map>(), ); @@ -2669,16 +2671,51 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi }); }); - const updateSubagentModel = Effect.fnUntraced(function* ( + const updateSubagentSelection = Effect.fnUntraced(function* ( nativeThreadId: string, value: string | null, + effort?: string | null, + tier?: string | null, ) { const model = value?.trim(); if (!model) return; - subagentModels.set(nativeThreadId, model); + const previous = subagentSelections.get(nativeThreadId); + const previousOptions = previous?.model === model ? previous.options : undefined; + const options = + effort === undefined && tier === undefined + ? previousOptions + : [ + ...(previousOptions ?? []).filter(({ id }) => + id === "reasoningEffort" + ? effort === undefined + : id === "serviceTier" && tier === undefined, + ), + ...[ + { id: "reasoningEffort", value: effort }, + { id: "serviceTier", value: tier }, + ].flatMap(({ id, value }) => + value?.trim() ? [{ id, value: value.trim() }] : [], + ), + ]; + const selection = { + model, + ...(options === undefined ? {} : { options }), + }; + subagentSelections.set(nativeThreadId, selection); const subagent = (yield* Ref.get(subagentThreads)).get(nativeThreadId); - if (subagent === undefined || subagent.task.model === model) return; - subagent.task = { ...subagent.task, model, updatedAt: yield* DateTime.now }; + if (subagent === undefined) return; + const modelSelection = { instanceId: subagent.task.providerInstanceId, ...selection }; + if ( + subagent.task.modelSelection && + modelSelectionsEqual(subagent.task.modelSelection, modelSelection) + ) + return; + subagent.task = { + ...subagent.task, + model, + modelSelection, + updatedAt: yield* DateTime.now, + }; yield* emitProviderEvent({ type: "subagent.updated", driver: CODEX_PROVIDER, @@ -2694,6 +2731,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi readonly prompt: string; readonly title: string | null; readonly model: string | null; + readonly reasoningEffort?: string | null | undefined; readonly ordinal: number; readonly emitInitialPrompt: boolean; }) => @@ -2702,6 +2740,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi if (registeredSubagents.has(input.nativeThreadId)) { return; } + if (input.model && !subagentSelections.has(input.nativeThreadId)) { + yield* updateSubagentSelection( + input.nativeThreadId, + input.model, + input.reasoningEffort, + ); + } const now = yield* DateTime.now; const subagentNodeId = idAllocator.derive.nodeFromProviderItem({ @@ -2744,6 +2789,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi createdAt: now, updatedAt: now, } satisfies OrchestrationV2ProviderThread; + const reportedSelection = subagentSelections.get(input.nativeThreadId); const task = { id: subagentNodeId, threadId: input.context.projectionThreadId, @@ -2758,7 +2804,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi nativeTaskRef: codexNativeItemRef(input.nativeItemId), prompt: input.prompt, title: input.title, - model: subagentModels.get(input.nativeThreadId) ?? input.model, + model: reportedSelection?.model ?? input.model, + modelSelection: reportedSelection + ? { + instanceId: input.context.input.modelSelection.instanceId, + ...reportedSelection, + } + : undefined, status: "running", result: null, startedAt: now, @@ -2913,9 +2965,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false }) .pipe( Effect.flatMap(decodeCodexChildThread), - Effect.map((response) => + Effect.map((response): typeof CodexChildSelection.Type | null => response.thread.id === input.nativeThreadId && response.thread.model?.trim() - ? { thread: response.thread, model: response.thread.model } + ? { + thread: response.thread, + model: response.thread.model, + reasoningEffort: response.thread.reasoningEffort, + } : null, ), Effect.catch(() => Effect.succeed(null)), @@ -2932,8 +2988,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi Effect.timeout("5 seconds"), Effect.flatMap((response) => response.thread.id === input.nativeThreadId && - !subagentModels.has(input.nativeThreadId) - ? updateSubagentModel(input.nativeThreadId, response.model) + !subagentSelections.has(input.nativeThreadId) + ? updateSubagentSelection( + input.nativeThreadId, + response.model, + response.reasoningEffort, + response.serviceTier, + ) : Effect.void, ), Effect.catch(() => Effect.void), @@ -2964,6 +3025,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi prompt: input.item.prompt ?? "", title: null, model, + reasoningEffort: input.item.reasoningEffort, ordinal: index + 1, emitInitialPrompt: true, }); @@ -4296,10 +4358,15 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi ); yield* client.handleServerNotification("thread/settings/updated", (payload) => - updateSubagentModel(payload.threadId, payload.threadSettings.model), + updateSubagentSelection( + payload.threadId, + payload.threadSettings.model, + payload.threadSettings.effort, + payload.threadSettings.serviceTier, + ), ); yield* client.handleServerNotification("model/rerouted", (payload) => - updateSubagentModel(payload.threadId, payload.toModel), + updateSubagentSelection(payload.threadId, payload.toModel), ); yield* client.handleServerNotification("turn/started", (payload) => diff --git a/apps/web/src/components/chat/SubagentTooltipContent.tsx b/apps/web/src/components/chat/SubagentTooltipContent.tsx index 61408d61f639..9bd96292cf9c 100644 --- a/apps/web/src/components/chat/SubagentTooltipContent.tsx +++ b/apps/web/src/components/chat/SubagentTooltipContent.tsx @@ -35,6 +35,7 @@ export function SubagentTooltipContent(props: { model: string | null; providerInstanceId: ProviderInstanceId; origin: OrchestrationV2Subagent["origin"]; + modelSelection?: OrchestrationV2Subagent["modelSelection"]; provider?: ServerProvider | undefined; /** The environment's instances; with several accounts on one provider, the card names this one. */ providers?: ReadonlyArray | undefined; @@ -57,7 +58,9 @@ export function SubagentTooltipContent(props: { ? resolveSelectableModel(props.provider.driver, model, props.provider.models) : model; const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug); - const childSelection = props.childThread?.modelSelection; + const childSelection = + props.modelSelection ?? + (props.origin === "app_owned" ? props.childThread?.modelSelection : undefined); const childModel = props.provider ? (resolveSelectableModel( props.provider.driver, @@ -67,9 +70,7 @@ export function SubagentTooltipContent(props: { : childSelection?.model.trim(); const provider = props.provider; const matchingSelection = - props.origin === "app_owned" && - childModel === (modelSlug ?? model) && - childSelection?.instanceId === props.providerInstanceId + childModel === (modelSlug ?? model) && childSelection?.instanceId === props.providerInstanceId ? childSelection : undefined; const effort = ["reasoningEffort", "effort", "reasoning", "variant"] diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx index 6382520cdfad..4eb47d328240 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx @@ -579,6 +579,39 @@ it("shows readable models and only differing workspace details in agent tooltips expect(text(true)).toContain("My GPT · Work account · high"); expect(text()).toContain("My GPT · Work account · Fast mode onhigh"); expect(text()).not.toContain("Personal account"); + state.shells = []; + for (const status of ["running", "completed", "failed"] as const) { + state.projection = { + ...projection, + subagents: [ + { + ...projection.subagents[0], + origin: "provider_native", + status, + modelSelection: { + instanceId: "codex", + model: "gpt-5.4", + options: [ + { id: "reasoningEffort", value: "low" }, + { id: "serviceTier", value: "ultrafast" }, + ], + }, + }, + ], + }; + await act(async () => renderer.update(cloneElement(panel))); + if (status === "completed") { + await act(async () => + renderer.root.findByProps({ type: "button", "aria-expanded": false }).props.onClick(), + ); + } + expect(text()).toContain("My GPT · Work account · Ultrafast mode onlow"); + expect(text()).not.toContain(" · high"); + } + await act(async () => + renderer.root.findByProps({ type: "button", "aria-expanded": true }).props.onClick(), + ); + state.projection = projection; state.configs.set("test", speedConfig); child.modelSelection.options = [{ id: "reasoningEffort", value: "high" }]; state.shells = [ diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index cd1265e46a60..1c996542203d 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -212,6 +212,7 @@ export function ThreadRelationshipsPanel(props: { driver: subagent.driver, providerInstanceId: subagent.providerInstanceId, origin: subagent.origin, + modelSelection: subagent.modelSelection, }, ]), ), @@ -412,6 +413,7 @@ export function ThreadRelationshipsPanel(props: { model={agent.model} providerInstanceId={agent.providerInstanceId} origin={agent.origin} + modelSelection={agent.modelSelection} provider={provider} providers={providers} driver={providerDriver} diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index faeeac04c393..597d074fdb52 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -522,6 +522,7 @@ function SubagentTimelineLink(props: { {...props} elapsed={agent ? : null} model={agent?.model ?? null} + modelSelection={agent?.modelSelection} status={liveStatus} result={agent?.result ?? props.result} progress={agent?.progress ?? props.progress} @@ -532,7 +533,11 @@ function SubagentTimelineLink(props: { } function SubagentTimelineTooltip( - props: Parameters[0] & { model: string | null; elapsed: ReactNode }, + props: Parameters[0] & { + model: string | null; + modelSelection: OrchestrationV2Subagent["modelSelection"]; + elapsed: ReactNode; + }, ) { const environmentId = props.parentRef.environmentId; const parent = useThreadShell(props.parentRef)?.source; @@ -549,6 +554,7 @@ function SubagentTimelineTooltip( model={props.model} providerInstanceId={props.providerInstanceId} origin={props.origin} + modelSelection={props.modelSelection} provider={props.provider} providers={props.providers} driver={props.driver} diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts index c26d5a9327e4..95968e629da6 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts @@ -675,6 +675,7 @@ export const OrchestrationV2Subagent = Schema.Struct({ prompt: Schema.String, title: Schema.NullOr(Schema.String), model: Schema.NullOr(Schema.String), + modelSelection: Schema.optional(ModelSelection), // Parent-wake policy for app-owned tasks: "always" offers a continuation on // every terminal (async delegations; queue_after_active sequences it behind // a live parent run), "settled_only" offers only when the parent has no From b8866958bf4cf5c03ccbd559ccc9e6662a76c6e6 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:14:07 +0200 Subject: [PATCH 2/2] fix(codex): keep child settings across model reroutes --- .../Adapters/CodexAdapterV2.test.ts | 32 +++++++++++++++---- .../Adapters/CodexAdapterV2.ts | 2 +- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index ab0281b49346..862810662310 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -7164,12 +7164,32 @@ describe("CodexAdapterV2 post-settle continuation", () => { }, }, }; + const initialSettings: CodexReplay.CodexAppServerReplayEntry = { + type: "emit_inbound", + frame: { + method: "thread/settings/updated", + params: { + threadId: RESUME_CHILD_THREAD, + threadSettings: { + model: "gpt-6-astra", + effort: "low", + serviceTier: "ultrafast", + modelProvider: "openai", + cwd: "/workspace", + approvalPolicy: "never", + approvalsReviewer: "auto_review", + collaborationMode: { mode: "default", settings: { model: "gpt-6-astra" } }, + sandboxPolicy: { type: "dangerFullAccess" }, + }, + }, + }, + }; const harness = yield* makeCodexReplayHarness( { ...resumeSubagentTranscript, entries: resumeSubagentTranscript.entries.flatMap((entry) => entry.type === "emit_inbound" && entry.label === "turn/completed/root" - ? [entry, notification] + ? [entry, initialSettings, notification] : [entry], ), }, @@ -7198,12 +7218,10 @@ describe("CodexAdapterV2 post-settle continuation", () => { yield* Deferred.succeed(releaseMetadata, undefined); yield* TestClock.adjust("30 seconds"); assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model); - if (method === "thread/settings/updated") { - assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [ - { id: "reasoningEffort", value: "low" }, - { id: "serviceTier", value: "ultrafast" }, - ]); - } + assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [ + { id: "reasoningEffort", value: "low" }, + { id: "serviceTier", value: "ultrafast" }, + ]); }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), ), ); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index c38814020607..fefc1ece4fc9 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -2680,7 +2680,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi const model = value?.trim(); if (!model) return; const previous = subagentSelections.get(nativeThreadId); - const previousOptions = previous?.model === model ? previous.options : undefined; + const previousOptions = previous?.options; const options = effort === undefined && tier === undefined ? previousOptions