authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-10-08 09:08:14+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 23:08:14-07:00
log57f96e69d39669f47e7310062222cd67adbb4912
tree49c2dd93c6ded17485b797e04baf8e89e7cc251c
parentff9eb8bd0c8e71f98969a58d5789889e07b57c8b
signature Signed by PGP key B5690EEEBB952194

fix(lineage): keep agent effort and speed after completion (#16925)


7 files changed, 171 insertions(+), 25 deletions(-)

apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+38-2
......@@ -7029,13 +7029,17 @@ describe("CodexAdapterV2 post-settle continuation", () => {
70297029 : {
70307030 thread: {
70317031 id: name.includes("wrong child") ? "other-child" : threadId,
7032 ...(name.startsWith("current Codex") ? { model: "gpt-6-sol" } : {}),
7032 ...(name.startsWith("current Codex")
7033 ? { model: "gpt-6-sol", reasoningEffort: "high" }
7034 : {}),
70337035 },
70347036 model: name.startsWith("current Codex")
70357037 ? null
70367038 : name === "wrong child"
70377039 ? "gpt-5.6-sol"
70387040 : model,
7041 reasoningEffort: "high",
7042 serviceTier: "priority",
70397043 },
70407044 ),
70417045 );
......@@ -7055,6 +7059,12 @@ describe("CodexAdapterV2 post-settle continuation", () => {
70557059 yield* TestClock.adjust("100 millis");
70567060 yield* harness.firstTerminal;
70577061 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
7062 if (model) {
7063 assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [
7064 { id: "reasoningEffort", value: "high" },
7065 ...(name.startsWith("current Codex") ? [] : [{ id: "serviceTier", value: "priority" }]),
7066 ]);
7067 }
70587068 assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2);
70597069 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
70607070 ),
......@@ -7142,6 +7152,8 @@ describe("CodexAdapterV2 post-settle continuation", () => {
71427152 threadId: RESUME_CHILD_THREAD,
71437153 threadSettings: {
71447154 model,
7155 effort: "low",
7156 serviceTier: "ultrafast",
71457157 modelProvider: "openai",
71467158 cwd: "/workspace",
71477159 approvalPolicy: "never",
......@@ -7152,12 +7164,32 @@ describe("CodexAdapterV2 post-settle continuation", () => {
71527164 },
71537165 },
71547166 };
7167 const initialSettings: CodexReplay.CodexAppServerReplayEntry = {
7168 type: "emit_inbound",
7169 frame: {
7170 method: "thread/settings/updated",
7171 params: {
7172 threadId: RESUME_CHILD_THREAD,
7173 threadSettings: {
7174 model: "gpt-6-astra",
7175 effort: "low",
7176 serviceTier: "ultrafast",
7177 modelProvider: "openai",
7178 cwd: "/workspace",
7179 approvalPolicy: "never",
7180 approvalsReviewer: "auto_review",
7181 collaborationMode: { mode: "default", settings: { model: "gpt-6-astra" } },
7182 sandboxPolicy: { type: "dangerFullAccess" },
7183 },
7184 },
7185 },
7186 };
71557187 const harness = yield* makeCodexReplayHarness(
71567188 {
71577189 ...resumeSubagentTranscript,
71587190 entries: resumeSubagentTranscript.entries.flatMap((entry) =>
71597191 entry.type === "emit_inbound" && entry.label === "turn/completed/root"
7160 ? [entry, notification]
7192 ? [entry, initialSettings, notification]
71617193 : [entry],
71627194 ),
71637195 },
......@@ -7186,6 +7218,10 @@ describe("CodexAdapterV2 post-settle continuation", () => {
71867218 yield* Deferred.succeed(releaseMetadata, undefined);
71877219 yield* TestClock.adjust("30 seconds");
71887220 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
7221 assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [
7222 { id: "reasoningEffort", value: "low" },
7223 { id: "serviceTier", value: "ultrafast" },
7224 ]);
71897225 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
71907226 ),
71917227 );
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+85-18
......@@ -1352,18 +1352,20 @@ const decodeCodexResumeMetadata = Schema.decodeUnknownEffect(
13521352 Schema.Struct({ thread: Schema.Struct({ id: Schema.String, updatedAt: Schema.Number }) }),
13531353);
13541354
1355const decodeCodexChildModel = Schema.decodeUnknownEffect(
1356 Schema.Struct({
1357 thread: Schema.Struct({ id: Schema.String }),
1358 model: Schema.NullOr(Schema.String),
1359 }),
1360);
1355const CodexChildSelection = Schema.Struct({
1356 thread: Schema.Struct({ id: Schema.String }),
1357 model: Schema.NullOr(Schema.String),
1358 reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
1359 serviceTier: Schema.optional(Schema.NullOr(Schema.String)),
1360});
1361const decodeCodexChildModel = Schema.decodeUnknownEffect(CodexChildSelection);
13611362
13621363const decodeCodexChildThread = Schema.decodeUnknownEffect(
13631364 Schema.Struct({
13641365 thread: Schema.Struct({
13651366 id: Schema.String,
13661367 model: Schema.optional(Schema.NullOr(Schema.String)),
1368 reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
13671369 }),
13681370 }),
13691371);
......@@ -1834,7 +1836,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
18341836 const pendingRootTurns = yield* Ref.make(new Map<string, ProviderAdapterV2TurnInput>());
18351837 const turnWaiters = yield* Ref.make(new Map<string, Deferred.Deferred<void, never>>());
18361838 const subagentThreads = yield* Ref.make(new Map<string, CodexSubagentThreadContext>());
1837 const subagentModels = new Map<string, string>();
1839 const subagentSelections = new Map<string, Omit<ModelSelection, "instanceId">>();
18381840 const pendingSubagentTurns = yield* Ref.make(
18391841 new Map<string, ReadonlyArray<PendingCodexSubagentTurnStarted>>(),
18401842 );
......@@ -2669,16 +2671,51 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
26692671 });
26702672 });
26712673
2672 const updateSubagentModel = Effect.fnUntraced(function* (
2674 const updateSubagentSelection = Effect.fnUntraced(function* (
26732675 nativeThreadId: string,
26742676 value: string | null,
2677 effort?: string | null,
2678 tier?: string | null,
26752679 ) {
26762680 const model = value?.trim();
26772681 if (!model) return;
2678 subagentModels.set(nativeThreadId, model);
2682 const previous = subagentSelections.get(nativeThreadId);
2683 const previousOptions = previous?.options;
2684 const options =
2685 effort === undefined && tier === undefined
2686 ? previousOptions
2687 : [
2688 ...(previousOptions ?? []).filter(({ id }) =>
2689 id === "reasoningEffort"
2690 ? effort === undefined
2691 : id === "serviceTier" && tier === undefined,
2692 ),
2693 ...[
2694 { id: "reasoningEffort", value: effort },
2695 { id: "serviceTier", value: tier },
2696 ].flatMap(({ id, value }) =>
2697 value?.trim() ? [{ id, value: value.trim() }] : [],
2698 ),
2699 ];
2700 const selection = {
2701 model,
2702 ...(options === undefined ? {} : { options }),
2703 };
2704 subagentSelections.set(nativeThreadId, selection);
26792705 const subagent = (yield* Ref.get(subagentThreads)).get(nativeThreadId);
2680 if (subagent === undefined || subagent.task.model === model) return;
2681 subagent.task = { ...subagent.task, model, updatedAt: yield* DateTime.now };
2706 if (subagent === undefined) return;
2707 const modelSelection = { instanceId: subagent.task.providerInstanceId, ...selection };
2708 if (
2709 subagent.task.modelSelection &&
2710 modelSelectionsEqual(subagent.task.modelSelection, modelSelection)
2711 )
2712 return;
2713 subagent.task = {
2714 ...subagent.task,
2715 model,
2716 modelSelection,
2717 updatedAt: yield* DateTime.now,
2718 };
26822719 yield* emitProviderEvent({
26832720 type: "subagent.updated",
26842721 driver: CODEX_PROVIDER,
......@@ -2694,6 +2731,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
26942731 readonly prompt: string;
26952732 readonly title: string | null;
26962733 readonly model: string | null;
2734 readonly reasoningEffort?: string | null | undefined;
26972735 readonly ordinal: number;
26982736 readonly emitInitialPrompt: boolean;
26992737 }) =>
......@@ -2702,6 +2740,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
27022740 if (registeredSubagents.has(input.nativeThreadId)) {
27032741 return;
27042742 }
2743 if (input.model && !subagentSelections.has(input.nativeThreadId)) {
2744 yield* updateSubagentSelection(
2745 input.nativeThreadId,
2746 input.model,
2747 input.reasoningEffort,
2748 );
2749 }
27052750
27062751 const now = yield* DateTime.now;
27072752 const subagentNodeId = idAllocator.derive.nodeFromProviderItem({
......@@ -2744,6 +2789,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
27442789 createdAt: now,
27452790 updatedAt: now,
27462791 } satisfies OrchestrationV2ProviderThread;
2792 const reportedSelection = subagentSelections.get(input.nativeThreadId);
27472793 const task = {
27482794 id: subagentNodeId,
27492795 threadId: input.context.projectionThreadId,
......@@ -2758,7 +2804,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
27582804 nativeTaskRef: codexNativeItemRef(input.nativeItemId),
27592805 prompt: input.prompt,
27602806 title: input.title,
2761 model: subagentModels.get(input.nativeThreadId) ?? input.model,
2807 model: reportedSelection?.model ?? input.model,
2808 modelSelection: reportedSelection
2809 ? {
2810 instanceId: input.context.input.modelSelection.instanceId,
2811 ...reportedSelection,
2812 }
2813 : undefined,
27622814 status: "running",
27632815 result: null,
27642816 startedAt: now,
......@@ -2913,9 +2965,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
29132965 .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false })
29142966 .pipe(
29152967 Effect.flatMap(decodeCodexChildThread),
2916 Effect.map((response) =>
2968 Effect.map((response): typeof CodexChildSelection.Type | null =>
29172969 response.thread.id === input.nativeThreadId && response.thread.model?.trim()
2918 ? { thread: response.thread, model: response.thread.model }
2970 ? {
2971 thread: response.thread,
2972 model: response.thread.model,
2973 reasoningEffort: response.thread.reasoningEffort,
2974 }
29192975 : null,
29202976 ),
29212977 Effect.catch(() => Effect.succeed(null)),
......@@ -2932,8 +2988,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
29322988 Effect.timeout("5 seconds"),
29332989 Effect.flatMap((response) =>
29342990 response.thread.id === input.nativeThreadId &&
2935 !subagentModels.has(input.nativeThreadId)
2936 ? updateSubagentModel(input.nativeThreadId, response.model)
2991 !subagentSelections.has(input.nativeThreadId)
2992 ? updateSubagentSelection(
2993 input.nativeThreadId,
2994 response.model,
2995 response.reasoningEffort,
2996 response.serviceTier,
2997 )
29372998 : Effect.void,
29382999 ),
29393000 Effect.catch(() => Effect.void),
......@@ -2964,6 +3025,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
29643025 prompt: input.item.prompt ?? "",
29653026 title: null,
29663027 model,
3028 reasoningEffort: input.item.reasoningEffort,
29673029 ordinal: index + 1,
29683030 emitInitialPrompt: true,
29693031 });
......@@ -4296,10 +4358,15 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
42964358 );
42974359
42984360 yield* client.handleServerNotification("thread/settings/updated", (payload) =>
4299 updateSubagentModel(payload.threadId, payload.threadSettings.model),
4361 updateSubagentSelection(
4362 payload.threadId,
4363 payload.threadSettings.model,
4364 payload.threadSettings.effort,
4365 payload.threadSettings.serviceTier,
4366 ),
43004367 );
43014368 yield* client.handleServerNotification("model/rerouted", (payload) =>
4302 updateSubagentModel(payload.threadId, payload.toModel),
4369 updateSubagentSelection(payload.threadId, payload.toModel),
43034370 );
43044371
43054372 yield* client.handleServerNotification("turn/started", (payload) =>
apps/web/src/components/chat/SubagentTooltipContent.tsx+5-4
......@@ -35,6 +35,7 @@ export function SubagentTooltipContent(props: {
3535 model: string | null;
3636 providerInstanceId: ProviderInstanceId;
3737 origin: OrchestrationV2Subagent["origin"];
38 modelSelection?: OrchestrationV2Subagent["modelSelection"];
3839 provider?: ServerProvider | undefined;
3940 /** The environment's instances; with several accounts on one provider, the card names this one. */
4041 providers?: ReadonlyArray<ServerProvider> | undefined;
......@@ -57,7 +58,9 @@ export function SubagentTooltipContent(props: {
5758 ? resolveSelectableModel(props.provider.driver, model, props.provider.models)
5859 : model;
5960 const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug);
60 const childSelection = props.childThread?.modelSelection;
61 const childSelection =
62 props.modelSelection ??
63 (props.origin === "app_owned" ? props.childThread?.modelSelection : undefined);
6164 const childModel = props.provider
6265 ? (resolveSelectableModel(
6366 props.provider.driver,
......@@ -67,9 +70,7 @@ export function SubagentTooltipContent(props: {
6770 : childSelection?.model.trim();
6871 const provider = props.provider;
6972 const matchingSelection =
70 props.origin === "app_owned" &&
71 childModel === (modelSlug ?? model) &&
72 childSelection?.instanceId === props.providerInstanceId
73 childModel === (modelSlug ?? model) && childSelection?.instanceId === props.providerInstanceId
7374 ? childSelection
7475 : undefined;
7576 const effort = ["reasoningEffort", "effort", "reasoning", "variant"]
apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx+33
......@@ -579,6 +579,39 @@ it("shows readable models and only differing workspace details in agent tooltips
579579 expect(text(true)).toContain("My GPT · Work account · high");
580580 expect(text()).toContain("My GPT · Work account · Fast mode onhigh");
581581 expect(text()).not.toContain("Personal account");
582 state.shells = [];
583 for (const status of ["running", "completed", "failed"] as const) {
584 state.projection = {
585 ...projection,
586 subagents: [
587 {
588 ...projection.subagents[0],
589 origin: "provider_native",
590 status,
591 modelSelection: {
592 instanceId: "codex",
593 model: "gpt-5.4",
594 options: [
595 { id: "reasoningEffort", value: "low" },
596 { id: "serviceTier", value: "ultrafast" },
597 ],
598 },
599 },
600 ],
601 };
602 await act(async () => renderer.update(cloneElement(panel)));
603 if (status === "completed") {
604 await act(async () =>
605 renderer.root.findByProps({ type: "button", "aria-expanded": false }).props.onClick(),
606 );
607 }
608 expect(text()).toContain("My GPT · Work account · Ultrafast mode onlow");
609 expect(text()).not.toContain(" · high");
610 }
611 await act(async () =>
612 renderer.root.findByProps({ type: "button", "aria-expanded": true }).props.onClick(),
613 );
614 state.projection = projection;
582615 state.configs.set("test", speedConfig);
583616 child.modelSelection.options = [{ id: "reasoningEffort", value: "high" }];
584617 state.shells = [
apps/web/src/components/chat/ThreadRelationshipsControl.tsx+2
......@@ -212,6 +212,7 @@ export function ThreadRelationshipsPanel(props: {
212212 driver: subagent.driver,
213213 providerInstanceId: subagent.providerInstanceId,
214214 origin: subagent.origin,
215 modelSelection: subagent.modelSelection,
215216 },
216217 ]),
217218 ),
......@@ -412,6 +413,7 @@ export function ThreadRelationshipsPanel(props: {
412413 model={agent.model}
413414 providerInstanceId={agent.providerInstanceId}
414415 origin={agent.origin}
416 modelSelection={agent.modelSelection}
415417 provider={provider}
416418 providers={providers}
417419 driver={providerDriver}
apps/web/src/components/chat/V2LifecycleRow.tsx+7-1
......@@ -522,6 +522,7 @@ function SubagentTimelineLink(props: {
522522 {...props}
523523 elapsed={agent ? <AgentElapsed agent={projectedSubagentsToRuntime([agent])[0]!} /> : null}
524524 model={agent?.model ?? null}
525 modelSelection={agent?.modelSelection}
525526 status={liveStatus}
526527 result={agent?.result ?? props.result}
527528 progress={agent?.progress ?? props.progress}
......@@ -532,7 +533,11 @@ function SubagentTimelineLink(props: {
532533}
533534
534535function SubagentTimelineTooltip(
535 props: Parameters<typeof SubagentTimelineLink>[0] & { model: string | null; elapsed: ReactNode },
536 props: Parameters<typeof SubagentTimelineLink>[0] & {
537 model: string | null;
538 modelSelection: OrchestrationV2Subagent["modelSelection"];
539 elapsed: ReactNode;
540 },
536541) {
537542 const environmentId = props.parentRef.environmentId;
538543 const parent = useThreadShell(props.parentRef)?.source;
......@@ -549,6 +554,7 @@ function SubagentTimelineTooltip(
549554 model={props.model}
550555 providerInstanceId={props.providerInstanceId}
551556 origin={props.origin}
557 modelSelection={props.modelSelection}
552558 provider={props.provider}
553559 providers={props.providers}
554560 driver={props.driver}
packages/contracts/src/orchestrationV2.ts+1
......@@ -675,6 +675,7 @@ export const OrchestrationV2Subagent = Schema.Struct({
675675 prompt: Schema.String,
676676 title: Schema.NullOr(Schema.String),
677677 model: Schema.NullOr(Schema.String),
678 modelSelection: Schema.optional(ModelSelection),
678679 // Parent-wake policy for app-owned tasks: "always" offers a continuation on
679680 // every terminal (async delegations; queue_after_active sequences it behind
680681 // a live parent run), "settled_only" offers only when the parent has no