From 1da1b73588057d19a98e06a23f6d62aac9341791 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:15:16 +0200 Subject: [PATCH 1/2] test(orchestration-v2): preserve known Cursor subagent models --- .../Adapters/AcpAdapterV2.test.ts | 14 +++++++ .../orchestration-v2/Adapters/AcpAdapterV2.ts | 2 +- .../Adapters/CodexAdapterV2.test.ts | 19 +++++++++- .../Adapters/CodexAdapterV2.testkit.ts | 4 +- .../Adapters/CodexAdapterV2.ts | 23 +++++++++++- .../Adapters/CursorAdapterV2.test.ts | 37 +++++++++++++++---- .../Adapters/CursorAdapterV2.ts | 2 +- 7 files changed, 85 insertions(+), 16 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 33b91614b4da..c4812fa4e2cf 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1037,6 +1037,18 @@ describe("AcpAdapterV2", () => { // Production thread 54aeb6d7 split after "(command". Metadata shapes // below were captured from live Devin sessions showy-mile/fragrant-chamomile. const updates = [ + { + sessionUpdate: "tool_call_update", + toolCallId: "child-a", + status: "in_progress", + _meta: { + "cognition.ai/subagent_started": { + agentId: "child-a", + title: "Map orchestration", + task: "Run pwd, then reply ONE.", + }, + }, + }, { sessionUpdate: "tool_call_update", toolCallId: "child-a", @@ -1181,6 +1193,8 @@ describe("AcpAdapterV2", () => { event.type === "subagent.updated" ? [event.subagent] : [], ); const task = tasks.at(-1); + assert.isNull(tasks[0]?.model); + assert.equal(task?.model, "SWE-1.7 Medium"); assert.equal(task?.status, "completed"); assert.equal(task?.result, "Final report: ONE"); const childMessages = new Map( diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts index bf01d9cb2d7d..6321655c96d5 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts @@ -2670,10 +2670,10 @@ export function makeAcpAdapterV2( nativeTaskRef: nativeItemRef, prompt: update.prompt, title: update.title, - model: update.model, result: null, startedAt: now, }), + model: update.model?.trim() || existing?.task.model || null, status: taskStatus, result: update.result ?? existing?.task.result ?? null, completedAt: acpSubagentStatusIsTerminal(taskStatus) ? now : null, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 27fadf2c4381..0a512a92cef2 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -6246,6 +6246,8 @@ describe("CodexAdapterV2 post-settle continuation", () => { }); it.effect.each([ + { name: "current Codex Sol", model: "gpt-6-sol" }, + { name: "current Codex wrong child", model: null }, { name: "Sol", model: "gpt-5.6-sol" }, { name: "Fable", model: "gpt-5.6-fable" }, { name: "Astra", model: "gpt-6-astra" }, @@ -6257,6 +6259,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { Effect.gen(function* () { const metadataRead = yield* Deferred.make(); const modelReported = yield* Deferred.make(); + let metadataRequests = 0; const harness = yield* makeCodexReplayHarness( resumeSubagentTranscript, (event) => @@ -6265,14 +6268,22 @@ describe("CodexAdapterV2 post-settle continuation", () => { : Effect.void, undefined, (threadId) => { + metadataRequests++; assert.equal(threadId, RESUME_CHILD_THREAD); return Deferred.succeed(metadataRead, undefined).pipe( Effect.as( name === "invalid" ? {} : { - thread: { id: name === "wrong child" ? "other-child" : threadId }, - model: name === "wrong child" ? "gpt-5.6-sol" : model, + thread: { + id: name.includes("wrong child") ? "other-child" : threadId, + ...(name.startsWith("current Codex") ? { model: "gpt-6-sol" } : {}), + }, + model: name.startsWith("current Codex") + ? null + : name === "wrong child" + ? "gpt-5.6-sol" + : model, }, ), ); @@ -6292,6 +6303,10 @@ describe("CodexAdapterV2 post-settle continuation", () => { yield* TestClock.adjust("100 millis"); yield* harness.firstTerminal; assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model); + assert.equal( + metadataRequests, + name.startsWith("current Codex") || name === "invalid" ? 1 : 2, + ); }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), ), ); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts index d449c1a51f6b..c50abc2dc15f 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts @@ -67,9 +67,9 @@ export function withCodexReplayChildMetadata( raw: { ...client.raw, request: (method, params) => - method === "thread/resume" && + (method === "thread/read" || method === "thread/resume") && Predicate.isObject(params) && - params.excludeTurns === true && + (method === "thread/read" ? params.includeTurns === false : params.excludeTurns === true) && typeof params.threadId === "string" && childThreadIds.has(params.threadId) ? readMetadata(params.threadId) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 88cd870f53c2..674ff9bf2327 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1239,6 +1239,15 @@ const decodeCodexChildModel = Schema.decodeUnknownEffect( }), ); +const decodeCodexChildThread = Schema.decodeUnknownEffect( + Schema.Struct({ + thread: Schema.Struct({ + id: Schema.String, + model: Schema.optional(Schema.NullOr(Schema.String)), + }), + }), +); + export const makeCodexAppServerSpawnCommand = Effect.fn( "CodexAdapterV2.makeCodexAppServerSpawnCommand", )(function* (input: { @@ -2661,9 +2670,19 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi } if (task.model === null) { yield* client.raw - .request("thread/resume", { threadId: input.nativeThreadId, excludeTurns: true }) + .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false }) .pipe( - Effect.flatMap(decodeCodexChildModel), + Effect.flatMap(decodeCodexChildThread), + Effect.flatMap((response) => + response.thread.model?.trim() + ? Effect.succeed({ thread: response.thread, model: response.thread.model }) + : client.raw + .request("thread/resume", { + threadId: input.nativeThreadId, + excludeTurns: true, + }) + .pipe(Effect.flatMap(decodeCodexChildModel)), + ), Effect.timeout("5 seconds"), Effect.flatMap((response) => response.thread.id === input.nativeThreadId && diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts index d31e91eac08c..e3b037e27aa5 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts @@ -39,12 +39,14 @@ import { isCursorCancellationError, loggedCursorAgentOptions } from "./CursorAge const decodeCursorSettings = Schema.decodeEffect(CursorSettings); describe("CursorAdapterV2", () => { - for (const { status, model } of [ - { status: "finished", model: undefined }, - { status: "cancelled", model: "claude-opus-4-6" }, - { status: "error", model: "custom-fable" }, + for (const { status, model, lateModel } of [ + { status: "finished", model: undefined, lateModel: undefined }, + { status: "cancelled", model: "claude-opus-4-6", lateModel: undefined }, + { status: "error", model: "custom-fable", lateModel: undefined }, + { status: "finished", model: undefined, lateModel: "gpt-6-sol" }, + { status: "finished", model: "gpt-6-sol", lateModel: null }, ] as const) { - it.effect(`settles missing task completions when the Cursor run is ${status}`, () => + it.effect(`projects Cursor tasks: ${status}, late model ${lateModel}`, () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -95,12 +97,24 @@ describe("CursorAdapterV2", () => { mode: "unspecified" as const, }, }; - for (const type of ["partial-tool-call", "tool-call-started"] as const) { + const updates = + lateModel !== undefined + ? (["tool-call-started", "tool-call-completed"] as const) + : (["partial-tool-call", "tool-call-started"] as const); + for (const type of updates) { yield* input.onDelta!({ type, modelCallId: "model-call", callId: "task-call", - toolCall: taskToolCall, + toolCall: { + ...taskToolCall, + args: { + ...taskToolCall.args, + ...(type === "tool-call-completed" + ? { model: lateModel ?? undefined } + : {}), + }, + }, }).pipe(Effect.orDie); } return { @@ -179,9 +193,16 @@ describe("CursorAdapterV2", () => { const rows = events.filter((event) => event.type === "subagent.updated"); assert.equal(rows[0]?.subagent.status, "running"); assert.equal(rows[0]?.subagent.model, model ?? null); + assert.equal(rows.at(-1)?.subagent.model, lateModel ?? model ?? null); assert.equal( rows.at(-1)?.subagent.status, - status === "finished" ? "idle" : status === "cancelled" ? "cancelled" : "failed", + lateModel !== undefined + ? "completed" + : status === "finished" + ? "idle" + : status === "cancelled" + ? "cancelled" + : "failed", ); assert.isNotNull(rows.at(-1)?.subagent.completedAt); }).pipe(Effect.scoped, Effect.provide(Layer.merge(NodeServices.layer, IdAllocator.layer))), diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts index bb9de20da64f..2c4d31afb0bf 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts @@ -1550,10 +1550,10 @@ export function makeCursorAdapterV2( }, prompt: args.prompt, title: args.description, - model: args.model?.trim() || null, result: null, startedAt: now, }), + model: args.model?.trim() || existing?.task.model || null, nativeTaskRef: { driver: CursorAgentSdk.CURSOR_PROVIDER, nativeId: input.callId, From 57d3ae0a70304a3c0fd4d000c674a206c5708bdf Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Tue, 6 Oct 2026 13:07:00 -0500 Subject: [PATCH 2/2] fix(server): normalize subagent models and recover child metadata --- .../Adapters/AcpAdapterV2.test.ts | 11 ++++ .../orchestration-v2/Adapters/AcpAdapterV2.ts | 2 +- .../Adapters/CodexAdapterV2.test.ts | 65 +++++++++++++++++-- .../Adapters/CodexAdapterV2.testkit.ts | 8 ++- .../Adapters/CodexAdapterV2.ts | 14 ++-- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 816bd26c437b..f589cda72bd0 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1046,6 +1046,7 @@ describe("AcpAdapterV2", () => { agentId: "child-a", title: "Map orchestration", task: "Run pwd, then reply ONE.", + model: " \t ", }, }, }, @@ -1194,6 +1195,16 @@ describe("AcpAdapterV2", () => { ); const task = tasks.at(-1); assert.isNull(tasks[0]?.model); + const childThread = events.find( + (event) => + event.type === "app_thread.created" && event.appThread.id === task?.childThreadId, + ); + assert.equal( + childThread?.type === "app_thread.created" + ? childThread.appThread.modelSelection.model + : undefined, + modelSelection.model, + ); assert.equal(task?.model, "SWE-1.7 Medium"); assert.equal(task?.status, "completed"); assert.equal(task?.result, "Final report: ONE"); diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts index 6321655c96d5..f17055cd8118 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts @@ -2709,7 +2709,7 @@ export function makeAcpAdapterV2( providerInstanceId: context.input.modelSelection.instanceId, modelSelection: { ...context.input.modelSelection, - model: update.model ?? context.input.modelSelection.model, + model: task.model ?? context.input.modelSelection.model, }, title: subagentThreadTitle({ parentTitle: context.input.appThread.title, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index fc038eb6e16f..7df54e6836a3 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -32,6 +32,7 @@ import { assert, describe, it } from "@effect/vitest"; import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as CodexClient from "effect-codex-app-server/client"; +import * as CodexError from "effect-codex-app-server/errors"; import * as CodexReplay from "effect-codex-app-server/replay"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; @@ -1619,7 +1620,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { transcript: CodexReplay.CodexAppServerReplayTranscript, onEvent: (event: ProviderAdapterV2Event) => Effect.Effect = () => Effect.void, onRequest: (method: string, params: unknown) => Effect.Effect = () => Effect.void, - readChildMetadata?: (threadId: string) => Effect.Effect, + readChildMetadata?: Parameters[2], ) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -6459,14 +6460,68 @@ describe("CodexAdapterV2 post-settle continuation", () => { yield* TestClock.adjust("100 millis"); yield* harness.firstTerminal; assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model); - assert.equal( - metadataRequests, - name.startsWith("current Codex") || name === "invalid" ? 1 : 2, - ); + assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2); }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), ), ); + it.effect.each(["failed", "malformed", "wrong child", "blank model"] as const)( + "resumes child metadata after a %s read", + (readResult) => + Effect.scoped( + Effect.gen(function* () { + const modelReported = yield* Deferred.make(); + const model = "gpt-6-sol"; + const metadataRequests: Array = []; + const harness = yield* makeCodexReplayHarness( + resumeSubagentTranscript, + (event) => + event.type === "subagent.updated" && event.subagent.model === model + ? Deferred.succeed(modelReported, undefined) + : Effect.void, + undefined, + (threadId, method) => { + assert.equal(threadId, RESUME_CHILD_THREAD); + metadataRequests.push(method); + if (method === "thread/resume") { + return Effect.succeed({ thread: { id: threadId }, model }); + } + switch (readResult) { + case "failed": + return Effect.fail( + new CodexError.CodexAppServerRequestError({ + code: -32000, + errorMessage: "Child metadata unavailable", + method, + }), + ); + case "malformed": + return Effect.succeed({}); + case "wrong child": + return Effect.succeed({ thread: { id: "other-child", model: "gpt-6-astra" } }); + case "blank model": + return Effect.succeed({ thread: { id: threadId, model: " \t " } }); + } + }, + ); + yield* harness.runtime.startTurn( + makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("attempt-child-model-fallback"), + text: RESUME_PROMPT, + }), + ); + yield* Deferred.await(modelReported); + yield* TestClock.adjust("100 millis"); + yield* harness.firstTerminal; + assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model); + assert.deepEqual(metadataRequests, ["thread/read", "thread/resume"]); + }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), + ), + ); + it.effect.each(["thread/settings/updated", "model/rerouted"] as const)( "keeps %s child metadata when an older lookup finishes later", (method) => diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts index c50abc2dc15f..792c078fe4a9 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts @@ -3,6 +3,7 @@ import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { type ProviderReplayTranscript } from "@t3tools/contracts"; import * as CodexClient from "effect-codex-app-server/client"; +import type * as CodexError from "effect-codex-app-server/errors"; import * as CodexReplay from "effect-codex-app-server/replay"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -45,7 +46,10 @@ export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplay export function withCodexReplayChildMetadata( client: CodexClient.CodexAppServerClient["Service"], transcript: CodexReplay.CodexAppServerReplayTranscript, - readMetadata: (threadId: string) => Effect.Effect = (threadId) => + readMetadata: ( + threadId: string, + method: "thread/read" | "thread/resume", + ) => Effect.Effect = (threadId) => Effect.succeed({ thread: { id: threadId }, model: null }), ): CodexClient.CodexAppServerClient["Service"] { const childThreadIds = new Set( @@ -72,7 +76,7 @@ export function withCodexReplayChildMetadata( (method === "thread/read" ? params.includeTurns === false : params.excludeTurns === true) && typeof params.threadId === "string" && childThreadIds.has(params.threadId) - ? readMetadata(params.threadId) + ? readMetadata(params.threadId, method) : client.raw.request(method, params), }, }; diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index ff474abb04dc..a55ac4c5950a 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -2673,15 +2673,21 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false }) .pipe( Effect.flatMap(decodeCodexChildThread), + Effect.map((response) => + response.thread.id === input.nativeThreadId && response.thread.model?.trim() + ? { thread: response.thread, model: response.thread.model } + : null, + ), + Effect.catch(() => Effect.succeed(null)), Effect.flatMap((response) => - response.thread.model?.trim() - ? Effect.succeed({ thread: response.thread, model: response.thread.model }) - : client.raw + response === null + ? client.raw .request("thread/resume", { threadId: input.nativeThreadId, excludeTurns: true, }) - .pipe(Effect.flatMap(decodeCodexChildModel)), + .pipe(Effect.flatMap(decodeCodexChildModel)) + : Effect.succeed(response), ), Effect.timeout("5 seconds"), Effect.flatMap((response) =>