feat(runtime): enable codex-native limited internal unlock
This commit is contained in:
parent
e83e3cbcc9
commit
b5dfa14868
22 changed files with 543 additions and 53 deletions
|
|
@ -100,6 +100,7 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
|
|||
throw new HttpBadRequestError('providerId must be anthropic, codex, or gemini');
|
||||
})();
|
||||
const prompt = assertOptionalString(payload.prompt, 'prompt');
|
||||
const providerBackendId = assertOptionalString(payload.providerBackendId, 'providerBackendId');
|
||||
const model = assertOptionalString(payload.model, 'model');
|
||||
const effort = assertOptionalEffort(payload.effort);
|
||||
const clearContext = assertOptionalBoolean(payload.clearContext, 'clearContext');
|
||||
|
|
@ -111,6 +112,9 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
|
|||
teamName,
|
||||
cwd: assertAbsoluteCwd(payload.cwd),
|
||||
providerId,
|
||||
...(providerBackendId && {
|
||||
providerBackendId,
|
||||
}),
|
||||
...(prompt && {
|
||||
prompt,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1126,6 +1126,25 @@ function parseOptionalMemberProviderId(
|
|||
return { valid: false, error: 'member providerId must be anthropic, codex, or gemini' };
|
||||
}
|
||||
|
||||
function parseOptionalProviderBackendId(
|
||||
value: unknown
|
||||
): { valid: true; value: string | undefined } | { valid: false; error: string } {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return { valid: true, value: undefined };
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return { valid: false, error: 'providerBackendId must be a string' };
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return { valid: true, value: undefined };
|
||||
}
|
||||
if (trimmed.length > 64) {
|
||||
return { valid: false, error: 'providerBackendId too long (max 64)' };
|
||||
}
|
||||
return { valid: true, value: trimmed };
|
||||
}
|
||||
|
||||
function parseOptionalMemberEffort(
|
||||
value: unknown
|
||||
): { valid: true; value: EffortLevel | undefined } | { valid: false; error: string } {
|
||||
|
|
@ -1219,6 +1238,10 @@ async function validateProvisioningRequest(
|
|||
if (payload.prompt !== undefined && typeof payload.prompt !== 'string') {
|
||||
return { valid: false, error: 'prompt must be a string' };
|
||||
}
|
||||
const providerBackendValidation = parseOptionalProviderBackendId(payload.providerBackendId);
|
||||
if (!providerBackendValidation.valid) {
|
||||
return { valid: false, error: providerBackendValidation.error };
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(cwd, { recursive: true });
|
||||
|
|
@ -1276,6 +1299,7 @@ async function validateProvisioningRequest(
|
|||
: payload.providerId === 'gemini'
|
||||
? 'gemini'
|
||||
: 'anthropic',
|
||||
providerBackendId: providerBackendValidation.value,
|
||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
|
||||
skipPermissions:
|
||||
|
|
@ -1385,6 +1409,10 @@ async function handleLaunchTeam(
|
|||
if (payload.model !== undefined && typeof payload.model !== 'string') {
|
||||
return { success: false, error: 'model must be a string' };
|
||||
}
|
||||
const providerBackendValidation = parseOptionalProviderBackendId(payload.providerBackendId);
|
||||
if (!providerBackendValidation.valid) {
|
||||
return { success: false, error: providerBackendValidation.error };
|
||||
}
|
||||
|
||||
// Detect draft team: team.meta.json exists but config.json doesn't.
|
||||
// This happens when user created team config without launching (launchTeam=false),
|
||||
|
|
@ -1403,7 +1431,8 @@ async function handleLaunchTeam(
|
|||
if (isDraft) {
|
||||
const meta = await teamMetaStore.getMeta(tn);
|
||||
const membersStore = new TeamMembersMetaStore();
|
||||
const members = await membersStore.getMembers(tn);
|
||||
const membersMeta = await membersStore.getMeta(tn);
|
||||
const members = membersMeta?.members ?? [];
|
||||
|
||||
const createRequest: TeamCreateRequest = {
|
||||
teamName: tn,
|
||||
|
|
@ -1422,6 +1451,10 @@ async function handleLaunchTeam(
|
|||
: meta?.providerId === 'gemini'
|
||||
? 'gemini'
|
||||
: 'anthropic',
|
||||
providerBackendId:
|
||||
providerBackendValidation.value ??
|
||||
meta?.providerBackendId ??
|
||||
membersMeta?.providerBackendId,
|
||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
|
||||
limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined,
|
||||
|
|
@ -1468,6 +1501,7 @@ async function handleLaunchTeam(
|
|||
: payload.providerId === 'gemini'
|
||||
? 'gemini'
|
||||
: 'anthropic',
|
||||
providerBackendId: providerBackendValidation.value,
|
||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
|
||||
clearContext: payload.clearContext === true ? true : undefined,
|
||||
|
|
@ -2552,6 +2586,10 @@ async function handleCreateConfig(
|
|||
return { success: false, error: 'cwd must be an absolute path' };
|
||||
}
|
||||
}
|
||||
const providerBackendValidation = parseOptionalProviderBackendId(payload.providerBackendId);
|
||||
if (!providerBackendValidation.valid) {
|
||||
return { success: false, error: providerBackendValidation.error };
|
||||
}
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
const members: TeamCreateConfigRequest['members'] = [];
|
||||
|
|
@ -2609,6 +2647,7 @@ async function handleCreateConfig(
|
|||
color: typeof payload.color === 'string' ? payload.color.trim() || undefined : undefined,
|
||||
members,
|
||||
cwd: typeof payload.cwd === 'string' ? payload.cwd.trim() || undefined : undefined,
|
||||
providerBackendId: providerBackendValidation.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -3884,7 +3923,8 @@ async function handleGetSavedRequest(
|
|||
}
|
||||
|
||||
const membersStore = new TeamMembersMetaStore();
|
||||
const members = await membersStore.getMembers(tn);
|
||||
const membersMeta = await membersStore.getMeta(tn);
|
||||
const members = membersMeta?.members ?? [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -3896,6 +3936,7 @@ async function handleGetSavedRequest(
|
|||
cwd: meta.cwd,
|
||||
prompt: meta.prompt,
|
||||
providerId: meta.providerId ?? 'anthropic',
|
||||
providerBackendId: meta.providerBackendId ?? membersMeta?.providerBackendId,
|
||||
model: meta.model,
|
||||
effort: meta.effort as TeamCreateRequest['effort'],
|
||||
skipPermissions: meta.skipPermissions,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ export class ProviderConnectionService {
|
|||
|
||||
async applyConfiguredConnectionEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
providerId: CliProviderId
|
||||
providerId: CliProviderId,
|
||||
runtimeBackendOverride?: string | null
|
||||
): Promise<NodeJS.ProcessEnv> {
|
||||
if (providerId === 'anthropic') {
|
||||
const authMode = this.getConfiguredAuthMode(providerId);
|
||||
|
|
@ -109,8 +110,8 @@ export class ProviderConnectionService {
|
|||
}
|
||||
|
||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend();
|
||||
if (!this.shouldExposeCodexConnectionModes()) {
|
||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
||||
if (!this.shouldExposeCodexConnectionModes(runtimeBackendOverride)) {
|
||||
delete env[CODEX_API_KEY_BETA_ENV_VAR];
|
||||
delete env.OPENAI_API_KEY;
|
||||
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
|
||||
|
|
@ -172,7 +173,8 @@ export class ProviderConnectionService {
|
|||
|
||||
async augmentConfiguredConnectionEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
providerId: CliProviderId
|
||||
providerId: CliProviderId,
|
||||
runtimeBackendOverride?: string | null
|
||||
): Promise<NodeJS.ProcessEnv> {
|
||||
if (providerId === 'anthropic') {
|
||||
if (this.getConfiguredAuthMode(providerId) !== 'api_key') {
|
||||
|
|
@ -191,8 +193,8 @@ export class ProviderConnectionService {
|
|||
}
|
||||
|
||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend();
|
||||
if (!this.shouldExposeCodexConnectionModes()) {
|
||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
||||
if (!this.shouldExposeCodexConnectionModes(runtimeBackendOverride)) {
|
||||
return env;
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +250,8 @@ export class ProviderConnectionService {
|
|||
|
||||
async getConfiguredConnectionIssue(
|
||||
env: NodeJS.ProcessEnv,
|
||||
providerId: CliProviderId
|
||||
providerId: CliProviderId,
|
||||
runtimeBackendOverride?: string | null
|
||||
): Promise<string | null> {
|
||||
if (providerId === 'anthropic') {
|
||||
if (this.getConfiguredAuthMode(providerId) !== 'api_key') {
|
||||
|
|
@ -270,8 +273,11 @@ export class ProviderConnectionService {
|
|||
}
|
||||
|
||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend();
|
||||
if (!this.shouldExposeCodexConnectionModes() || codexConnection.authMode !== 'api_key') {
|
||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
||||
if (
|
||||
!this.shouldExposeCodexConnectionModes(runtimeBackendOverride) ||
|
||||
codexConnection.authMode !== 'api_key'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -294,12 +300,17 @@ export class ProviderConnectionService {
|
|||
|
||||
async getConfiguredConnectionIssues(
|
||||
env: NodeJS.ProcessEnv,
|
||||
providerIds: readonly CliProviderId[] = ['anthropic', 'codex', 'gemini']
|
||||
providerIds: readonly CliProviderId[] = ['anthropic', 'codex', 'gemini'],
|
||||
runtimeBackendOverrides?: Partial<Record<CliProviderId, string>>
|
||||
): Promise<Partial<Record<CliProviderId, string>>> {
|
||||
const issues: Partial<Record<CliProviderId, string>> = {};
|
||||
|
||||
for (const providerId of providerIds) {
|
||||
const issue = await this.getConfiguredConnectionIssue(env, providerId);
|
||||
const issue = await this.getConfiguredConnectionIssue(
|
||||
env,
|
||||
providerId,
|
||||
runtimeBackendOverrides?.[providerId]
|
||||
);
|
||||
if (issue) {
|
||||
issues[providerId] = issue;
|
||||
}
|
||||
|
|
@ -369,15 +380,25 @@ export class ProviderConnectionService {
|
|||
return this.apiKeyService.lookupPreferred(envVarName);
|
||||
}
|
||||
|
||||
private getConfiguredCodexRuntimeBackend(): 'auto' | 'adapter' | 'api' | 'codex-native' {
|
||||
private getConfiguredCodexRuntimeBackend(
|
||||
runtimeBackendOverride?: string | null
|
||||
): 'auto' | 'adapter' | 'api' | 'codex-native' {
|
||||
if (
|
||||
runtimeBackendOverride === 'auto' ||
|
||||
runtimeBackendOverride === 'adapter' ||
|
||||
runtimeBackendOverride === 'api' ||
|
||||
runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID
|
||||
) {
|
||||
return runtimeBackendOverride;
|
||||
}
|
||||
return this.configManager.getConfig().runtime.providerBackends.codex;
|
||||
}
|
||||
|
||||
private shouldExposeCodexConnectionModes(): boolean {
|
||||
private shouldExposeCodexConnectionModes(runtimeBackendOverride?: string | null): boolean {
|
||||
const config = this.configManager.getConfig();
|
||||
return (
|
||||
config.providerConnections.codex.apiKeyBetaEnabled ||
|
||||
config.runtime.providerBackends.codex === CODEX_NATIVE_BACKEND_ID
|
||||
this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride) === CODEX_NATIVE_BACKEND_ID
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type ProviderEnvTargetId = CliProviderId | TeamProviderId | undefined;
|
|||
export interface ProviderAwareCliEnvOptions {
|
||||
binaryPath?: string | null;
|
||||
providerId?: ProviderEnvTargetId;
|
||||
providerBackendId?: string | null;
|
||||
shellEnv?: NodeJS.ProcessEnv | null;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
connectionMode?: 'strict' | 'augment';
|
||||
|
|
@ -71,21 +72,39 @@ export async function buildProviderAwareCliEnv(
|
|||
if (options.providerId) {
|
||||
const resolvedProviderId = resolveTeamProviderId(options.providerId);
|
||||
applyProviderRuntimeEnv(env, options.providerId);
|
||||
if (resolvedProviderId === 'codex' && options.providerBackendId?.trim()) {
|
||||
env.CLAUDE_CODE_CODEX_BACKEND = options.providerBackendId.trim();
|
||||
}
|
||||
if (resolvedProviderId === 'gemini' && options.providerBackendId?.trim()) {
|
||||
env.CLAUDE_CODE_GEMINI_BACKEND = options.providerBackendId.trim();
|
||||
}
|
||||
if (connectionMode === 'augment') {
|
||||
await providerConnectionService.augmentConfiguredConnectionEnv(env, resolvedProviderId);
|
||||
await providerConnectionService.augmentConfiguredConnectionEnv(
|
||||
env,
|
||||
resolvedProviderId,
|
||||
options.providerBackendId
|
||||
);
|
||||
return {
|
||||
env,
|
||||
connectionIssues: {},
|
||||
};
|
||||
}
|
||||
|
||||
await providerConnectionService.applyConfiguredConnectionEnv(env, resolvedProviderId);
|
||||
await providerConnectionService.applyConfiguredConnectionEnv(
|
||||
env,
|
||||
resolvedProviderId,
|
||||
options.providerBackendId
|
||||
);
|
||||
|
||||
return {
|
||||
env,
|
||||
connectionIssues: await providerConnectionService.getConfiguredConnectionIssues(env, [
|
||||
resolvedProviderId,
|
||||
]),
|
||||
connectionIssues: await providerConnectionService.getConfiguredConnectionIssues(
|
||||
env,
|
||||
[resolvedProviderId],
|
||||
resolvedProviderId === 'codex' || resolvedProviderId === 'gemini'
|
||||
? { [resolvedProviderId]: options.providerBackendId?.trim() || undefined }
|
||||
: undefined
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|||
|
||||
const TEAM_ROOT_FILES = [
|
||||
'config.json',
|
||||
'team.meta.json',
|
||||
'kanban-state.json',
|
||||
'sentMessages.json',
|
||||
'sent-cross-team.json',
|
||||
|
|
|
|||
|
|
@ -2319,6 +2319,7 @@ export class TeamDataService {
|
|||
description: request.description,
|
||||
color: request.color,
|
||||
cwd: request.cwd?.trim() || '',
|
||||
providerBackendId: request.providerBackendId,
|
||||
createdAt: joinedAt,
|
||||
});
|
||||
|
||||
|
|
@ -2349,7 +2350,10 @@ export class TeamDataService {
|
|||
agentType: 'general-purpose',
|
||||
color: getMemberColorByName(member.name.trim()),
|
||||
joinedAt,
|
||||
}))
|
||||
})),
|
||||
{
|
||||
providerBackendId: request.providerBackendId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,22 @@ import { atomicWriteAsync } from './atomicWrite';
|
|||
|
||||
import type { TeamMember } from '@shared/types';
|
||||
|
||||
interface TeamMembersMetaFile {
|
||||
export interface TeamMembersMetaFile {
|
||||
version: 1;
|
||||
providerBackendId?: string;
|
||||
members: TeamMember[];
|
||||
}
|
||||
|
||||
const MAX_META_FILE_BYTES = 256 * 1024;
|
||||
|
||||
function normalizeOptionalBackendId(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeMember(member: TeamMember): TeamMember | null {
|
||||
const trimmedName = member.name?.trim();
|
||||
if (!trimmedName) {
|
||||
|
|
@ -45,15 +54,15 @@ export class TeamMembersMetaStore {
|
|||
return path.join(getTeamsBasePath(), teamName, 'members.meta.json');
|
||||
}
|
||||
|
||||
async getMembers(teamName: string): Promise<TeamMember[]> {
|
||||
async getMeta(teamName: string): Promise<TeamMembersMetaFile | null> {
|
||||
const metaPath = this.getMetaPath(teamName);
|
||||
try {
|
||||
const stat = await fs.promises.stat(metaPath);
|
||||
if (!stat.isFile()) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
if (stat.isFile() && stat.size > MAX_META_FILE_BYTES) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// ignore - readFile below will handle ENOENT and throw on other errors
|
||||
|
|
@ -63,10 +72,10 @@ export class TeamMembersMetaStore {
|
|||
raw = await readFileUtf8WithTimeout(metaPath, 5_000);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
if (error instanceof FileReadTimeoutError) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -75,15 +84,15 @@ export class TeamMembersMetaStore {
|
|||
try {
|
||||
parsed = JSON.parse(raw) as unknown;
|
||||
} catch {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
const file = parsed as Partial<TeamMembersMetaFile>;
|
||||
if (!Array.isArray(file.members)) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
const deduped = new Map<string, TeamMember>();
|
||||
|
|
@ -107,10 +116,22 @@ export class TeamMembersMetaStore {
|
|||
}
|
||||
}
|
||||
|
||||
return Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
return {
|
||||
version: 1,
|
||||
providerBackendId: normalizeOptionalBackendId(file.providerBackendId),
|
||||
members: Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
};
|
||||
}
|
||||
|
||||
async writeMembers(teamName: string, members: TeamMember[]): Promise<void> {
|
||||
async getMembers(teamName: string): Promise<TeamMember[]> {
|
||||
return (await this.getMeta(teamName))?.members ?? [];
|
||||
}
|
||||
|
||||
async writeMembers(
|
||||
teamName: string,
|
||||
members: TeamMember[],
|
||||
options?: { providerBackendId?: string }
|
||||
): Promise<void> {
|
||||
const deduped = new Map<string, TeamMember>();
|
||||
for (const member of members) {
|
||||
const normalized = normalizeMember(member);
|
||||
|
|
@ -131,6 +152,7 @@ export class TeamMembersMetaStore {
|
|||
|
||||
const payload: TeamMembersMetaFile = {
|
||||
version: 1,
|
||||
providerBackendId: normalizeOptionalBackendId(options?.providerBackendId),
|
||||
members: Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface TeamMetaFile {
|
|||
cwd: string;
|
||||
prompt?: string;
|
||||
providerId?: 'anthropic' | 'codex' | 'gemini';
|
||||
providerBackendId?: string;
|
||||
model?: string;
|
||||
effort?: string;
|
||||
skipPermissions?: boolean;
|
||||
|
|
@ -30,6 +31,14 @@ export interface TeamMetaFile {
|
|||
|
||||
const MAX_META_FILE_BYTES = 256 * 1024;
|
||||
|
||||
function normalizeOptionalBackendId(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export class TeamMetaStore {
|
||||
private getMetaPath(teamName: string): string {
|
||||
return path.join(getTeamsBasePath(), teamName, 'team.meta.json');
|
||||
|
|
@ -89,6 +98,7 @@ export class TeamMetaStore {
|
|||
file.providerId === 'gemini'
|
||||
? file.providerId
|
||||
: undefined,
|
||||
providerBackendId: normalizeOptionalBackendId(file.providerBackendId),
|
||||
model: typeof file.model === 'string' ? file.model.trim() || undefined : undefined,
|
||||
effort: typeof file.effort === 'string' ? file.effort.trim() || undefined : undefined,
|
||||
skipPermissions: typeof file.skipPermissions === 'boolean' ? file.skipPermissions : undefined,
|
||||
|
|
@ -109,6 +119,7 @@ export class TeamMetaStore {
|
|||
cwd: data.cwd.trim(),
|
||||
prompt: data.prompt?.trim() || undefined,
|
||||
providerId: data.providerId,
|
||||
providerBackendId: normalizeOptionalBackendId(data.providerBackendId),
|
||||
model: data.model?.trim() || undefined,
|
||||
effort: data.effort?.trim() || undefined,
|
||||
skipPermissions: data.skipPermissions,
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ function mergeProvisioningWarnings(
|
|||
}
|
||||
|
||||
function buildRuntimeLaunchWarning(
|
||||
request: Pick<TeamCreateRequest, 'providerId' | 'model' | 'effort'>,
|
||||
request: Pick<TeamCreateRequest, 'providerId' | 'providerBackendId' | 'model' | 'effort'>,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options?: {
|
||||
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
||||
|
|
@ -420,7 +420,7 @@ function buildRuntimeLaunchWarning(
|
|||
const providerLabel = getTeamProviderLabel(providerId);
|
||||
const modelLabel = request.model?.trim() || 'default';
|
||||
const effortLabel = request.effort ?? 'default';
|
||||
const backend = getConfiguredRuntimeBackend(providerId);
|
||||
const backend = request.providerBackendId?.trim() || getConfiguredRuntimeBackend(providerId);
|
||||
const flags: string[] = [];
|
||||
if (env.CLAUDE_CODE_USE_GEMINI === '1') flags.push('USE_GEMINI');
|
||||
if (env.CLAUDE_CODE_USE_OPENAI === '1') flags.push('USE_OPENAI');
|
||||
|
|
@ -455,7 +455,7 @@ function logRuntimeLaunchSnapshot(
|
|||
teamName: string,
|
||||
claudePath: string,
|
||||
args: string[],
|
||||
request: Pick<TeamCreateRequest, 'providerId' | 'model' | 'effort'>,
|
||||
request: Pick<TeamCreateRequest, 'providerId' | 'providerBackendId' | 'model' | 'effort'>,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options?: {
|
||||
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
||||
|
|
@ -466,9 +466,10 @@ function logRuntimeLaunchSnapshot(
|
|||
const providerId = resolveTeamProviderId(request.providerId);
|
||||
const snapshot = {
|
||||
providerId,
|
||||
providerBackendId: request.providerBackendId ?? null,
|
||||
model: request.model ?? null,
|
||||
effort: request.effort ?? null,
|
||||
configuredBackend: getConfiguredRuntimeBackend(providerId),
|
||||
configuredBackend: request.providerBackendId?.trim() || getConfiguredRuntimeBackend(providerId),
|
||||
promptSize: options?.promptSize ?? null,
|
||||
expectedMembersCount: options?.expectedMembersCount ?? null,
|
||||
geminiRuntimeAuth:
|
||||
|
|
@ -1149,14 +1150,24 @@ function buildEffectiveTeamMemberSpecs(
|
|||
}
|
||||
|
||||
function shouldSkipResumeForProviderRuntimeChange(
|
||||
request: Pick<TeamLaunchRequest, 'providerId' | 'model'>,
|
||||
config: Record<string, unknown>
|
||||
request: Pick<TeamLaunchRequest, 'providerId' | 'providerBackendId' | 'model'>,
|
||||
config: Record<string, unknown>,
|
||||
persistedProviderBackendId?: string | null
|
||||
): { skip: boolean; reason?: string } {
|
||||
const providerId = normalizeTeamMemberProviderId(request.providerId);
|
||||
if (providerId !== 'gemini' && providerId !== 'codex') {
|
||||
return { skip: false };
|
||||
}
|
||||
|
||||
const requestedBackendId = request.providerBackendId?.trim() || null;
|
||||
const previousBackendId = persistedProviderBackendId?.trim() || null;
|
||||
if (requestedBackendId && previousBackendId && requestedBackendId !== previousBackendId) {
|
||||
return {
|
||||
skip: true,
|
||||
reason: `runtime backend changed (${previousBackendId} -> ${requestedBackendId})`,
|
||||
};
|
||||
}
|
||||
|
||||
const members = Array.isArray(config.members)
|
||||
? (config.members as Record<string, unknown>[])
|
||||
: [];
|
||||
|
|
@ -4193,6 +4204,7 @@ export class TeamProvisioningService {
|
|||
const updatedAt = nowIso();
|
||||
const runId = this.getTrackedRunId(teamName);
|
||||
const run = runId ? (this.runs.get(runId) ?? null) : null;
|
||||
const persistedTeamMeta = await this.teamMetaStore.getMeta(teamName).catch(() => null);
|
||||
|
||||
let configuredMembers: TeamConfig['members'] = [];
|
||||
try {
|
||||
|
|
@ -4294,6 +4306,7 @@ export class TeamProvisioningService {
|
|||
teamName,
|
||||
updatedAt,
|
||||
runId: run?.runId ?? null,
|
||||
providerBackendId: run?.request.providerBackendId ?? persistedTeamMeta?.providerBackendId,
|
||||
members: snapshotMembers,
|
||||
};
|
||||
|
||||
|
|
@ -5838,7 +5851,10 @@ export class TeamProvisioningService {
|
|||
throw new Error('Claude CLI not found; install it or provide a valid path');
|
||||
}
|
||||
|
||||
const provisioningEnv = await this.buildProvisioningEnv(request.providerId);
|
||||
const provisioningEnv = await this.buildProvisioningEnv(
|
||||
request.providerId,
|
||||
request.providerBackendId
|
||||
);
|
||||
const { env: shellEnv, geminiRuntimeAuth, warning: envWarning } = provisioningEnv;
|
||||
if (envWarning) {
|
||||
throw new Error(envWarning);
|
||||
|
|
@ -6042,6 +6058,7 @@ export class TeamProvisioningService {
|
|||
cwd: request.cwd,
|
||||
prompt: request.prompt,
|
||||
providerId: request.providerId,
|
||||
providerBackendId: request.providerBackendId,
|
||||
model: request.model,
|
||||
effort: request.effort,
|
||||
skipPermissions: request.skipPermissions,
|
||||
|
|
@ -6065,7 +6082,10 @@ export class TeamProvisioningService {
|
|||
agentType: 'general-purpose' as const,
|
||||
color: getMemberColorByName(m.name.trim()),
|
||||
joinedAt: Date.now(),
|
||||
}))
|
||||
})),
|
||||
{
|
||||
providerBackendId: request.providerBackendId,
|
||||
}
|
||||
);
|
||||
if (request.skipPermissions === false) {
|
||||
await this.seedTeammateOperationalPermissionRules(request.teamName, request.cwd);
|
||||
|
|
@ -6310,7 +6330,14 @@ export class TeamProvisioningService {
|
|||
if (!skipResume) {
|
||||
try {
|
||||
const configParsed = JSON.parse(configRaw) as Record<string, unknown>;
|
||||
const resumeGuard = shouldSkipResumeForProviderRuntimeChange(request, configParsed);
|
||||
const persistedTeamMeta = await this.teamMetaStore
|
||||
.getMeta(request.teamName)
|
||||
.catch(() => null);
|
||||
const resumeGuard = shouldSkipResumeForProviderRuntimeChange(
|
||||
request,
|
||||
configParsed,
|
||||
persistedTeamMeta?.providerBackendId ?? null
|
||||
);
|
||||
if (resumeGuard.skip) {
|
||||
logger.info(
|
||||
`[${request.teamName}] Skipping session resume — ${resumeGuard.reason ?? 'runtime changed'}`
|
||||
|
|
@ -6395,7 +6422,10 @@ export class TeamProvisioningService {
|
|||
const runId = randomUUID();
|
||||
const startedAt = nowIso();
|
||||
|
||||
const provisioningEnv = await this.buildProvisioningEnv(request.providerId);
|
||||
const provisioningEnv = await this.buildProvisioningEnv(
|
||||
request.providerId,
|
||||
request.providerBackendId
|
||||
);
|
||||
const { env: shellEnv, geminiRuntimeAuth, warning: envWarning } = provisioningEnv;
|
||||
if (envWarning) {
|
||||
throw new Error(envWarning);
|
||||
|
|
@ -6421,6 +6451,7 @@ export class TeamProvisioningService {
|
|||
members: effectiveMemberSpecs,
|
||||
cwd: request.cwd,
|
||||
providerId: request.providerId,
|
||||
providerBackendId: request.providerBackendId,
|
||||
model: request.model,
|
||||
effort: request.effort,
|
||||
skipPermissions: request.skipPermissions,
|
||||
|
|
@ -6643,6 +6674,42 @@ export class TeamProvisioningService {
|
|||
});
|
||||
// --resume is added above when a valid previous session JSONL exists.
|
||||
// Without it, CLI creates a fresh session ID automatically.
|
||||
await this.teamMetaStore.writeMeta(request.teamName, {
|
||||
displayName: syntheticRequest.displayName,
|
||||
description: syntheticRequest.description,
|
||||
color: syntheticRequest.color,
|
||||
cwd: request.cwd,
|
||||
prompt: request.prompt,
|
||||
providerId: request.providerId,
|
||||
providerBackendId: request.providerBackendId,
|
||||
model: request.model,
|
||||
effort: request.effort,
|
||||
skipPermissions: request.skipPermissions,
|
||||
worktree: request.worktree,
|
||||
extraCliArgs: request.extraCliArgs,
|
||||
limitContext: request.limitContext,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
await this.membersMetaStore.writeMembers(
|
||||
request.teamName,
|
||||
effectiveMemberSpecs.map((member) => ({
|
||||
name: member.name.trim(),
|
||||
role: member.role?.trim() || undefined,
|
||||
workflow: member.workflow?.trim() || undefined,
|
||||
providerId: normalizeOptionalTeamProviderId(member.providerId),
|
||||
model: member.model?.trim() || undefined,
|
||||
effort:
|
||||
member.effort === 'low' || member.effort === 'medium' || member.effort === 'high'
|
||||
? member.effort
|
||||
: undefined,
|
||||
agentType: 'general-purpose',
|
||||
color: getMemberColorByName(member.name.trim()),
|
||||
joinedAt: Date.now(),
|
||||
})),
|
||||
{
|
||||
providerBackendId: request.providerBackendId,
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
if (request.skipPermissions === false) {
|
||||
|
|
@ -11253,9 +11320,6 @@ export class TeamProvisioningService {
|
|||
}
|
||||
);
|
||||
|
||||
// Clean up team.meta.json — provisioning succeeded, config.json is now authoritative.
|
||||
await this.teamMetaStore.deleteMeta(run.teamName).catch(() => {});
|
||||
|
||||
// Audit: flag any expected member not registered in config.json after provisioning.
|
||||
await this.refreshMemberSpawnStatusesFromLeadInbox(run);
|
||||
await this.maybeAuditMemberSpawnStatuses(run, { force: true });
|
||||
|
|
@ -12182,7 +12246,8 @@ export class TeamProvisioningService {
|
|||
}
|
||||
|
||||
private async buildProvisioningEnv(
|
||||
providerId: TeamProviderId | undefined = 'anthropic'
|
||||
providerId: TeamProviderId | undefined = 'anthropic',
|
||||
providerBackendId?: string | null
|
||||
): Promise<ProvisioningEnvResolution> {
|
||||
const shellEnv = await resolveInteractiveShellEnv();
|
||||
// getHomeDir() uses Electron's app.getPath('home') which handles Unicode
|
||||
|
|
@ -12228,6 +12293,7 @@ export class TeamProvisioningService {
|
|||
const resolvedProviderId = resolveTeamProviderId(providerId);
|
||||
const providerEnvResult = await buildProviderAwareCliEnv({
|
||||
providerId,
|
||||
providerBackendId,
|
||||
shellEnv,
|
||||
env,
|
||||
});
|
||||
|
|
@ -13059,7 +13125,10 @@ export class TeamProvisioningService {
|
|||
agentType: 'general-purpose',
|
||||
color: getMemberColorByName(member.name.trim()),
|
||||
joinedAt,
|
||||
}))
|
||||
})),
|
||||
{
|
||||
providerBackendId: request.providerBackendId,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
normalizeCreateLaunchProviderForUi,
|
||||
} from '@renderer/utils/geminiUiFreeze';
|
||||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||
import { resolveEffectiveProviderBackendId } from '@renderer/utils/providerBackendIdentity';
|
||||
import {
|
||||
getTeamModelSelectionError,
|
||||
normalizeExplicitTeamModelForUi,
|
||||
|
|
@ -972,6 +973,9 @@ export const CreateTeamDialog = ({
|
|||
cwd: effectiveCwd,
|
||||
prompt: prompt.trim() || undefined,
|
||||
providerId: selectedProviderId,
|
||||
providerBackendId:
|
||||
resolveEffectiveProviderBackendId(runtimeProviderStatusById.get(selectedProviderId)) ??
|
||||
undefined,
|
||||
model: effectiveModel,
|
||||
effort: (selectedEffort as EffortLevel) || undefined,
|
||||
limitContext,
|
||||
|
|
@ -988,6 +992,7 @@ export const CreateTeamDialog = ({
|
|||
effectiveCwd,
|
||||
prompt,
|
||||
selectedProviderId,
|
||||
runtimeProviderStatusById,
|
||||
effectiveModel,
|
||||
selectedEffort,
|
||||
limitContext,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
normalizeCreateLaunchProviderForUi,
|
||||
} from '@renderer/utils/geminiUiFreeze';
|
||||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||
import { resolveEffectiveProviderBackendId } from '@renderer/utils/providerBackendIdentity';
|
||||
import { nameColorSet } from '@renderer/utils/projectColor';
|
||||
import {
|
||||
getTeamModelSelectionError,
|
||||
|
|
@ -319,6 +320,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
);
|
||||
const members = isLaunch ? props.members : storeMembers;
|
||||
const [savedLaunchProviderId, setSavedLaunchProviderId] = useState<TeamProviderId | null>(null);
|
||||
const [savedLaunchProviderBackendId, setSavedLaunchProviderBackendId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// Advanced CLI section state (with localStorage persistence)
|
||||
const [worktreeEnabled, setWorktreeEnabledRaw] = useState(
|
||||
|
|
@ -623,6 +627,11 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
: savedRequest?.providerId === 'anthropic'
|
||||
? 'anthropic'
|
||||
: null;
|
||||
const savedProviderBackendId =
|
||||
typeof savedRequest?.providerBackendId === 'string' &&
|
||||
savedRequest.providerBackendId.trim().length > 0
|
||||
? savedRequest.providerBackendId.trim()
|
||||
: null;
|
||||
const storedProviderId = normalizeProviderForMode(getStoredTeamProvider(), multimodelEnabled);
|
||||
const launchPrefill = resolveLaunchDialogPrefill({
|
||||
members,
|
||||
|
|
@ -635,6 +644,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
getStoredModel: getStoredTeamModel,
|
||||
});
|
||||
setSavedLaunchProviderId(savedProviderId);
|
||||
setSavedLaunchProviderBackendId(
|
||||
launchPrefill.providerBackendId ?? savedProviderBackendId ?? null
|
||||
);
|
||||
|
||||
setMembersDrafts(
|
||||
createMemberDraftsFromInputs(editableMembersSource).map((member) =>
|
||||
|
|
@ -1390,6 +1402,13 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
cwd: effectiveCwd,
|
||||
prompt: promptDraft.value.trim() || undefined,
|
||||
providerId: selectedProviderId,
|
||||
providerBackendId:
|
||||
resolveEffectiveProviderBackendId(
|
||||
runtimeProviderStatusById.get(selectedProviderId)
|
||||
) ??
|
||||
previousLaunchParams?.providerBackendId ??
|
||||
savedLaunchProviderBackendId ??
|
||||
undefined,
|
||||
model: computeEffectiveTeamModel(selectedModel, limitContext, selectedProviderId),
|
||||
effort: (selectedEffort as EffortLevel) || undefined,
|
||||
limitContext,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { ResolvedTeamMember, TeamCreateRequest, TeamProviderId } from '@sha
|
|||
|
||||
interface PreviousLaunchParamsLike {
|
||||
providerId?: TeamProviderId;
|
||||
providerBackendId?: string;
|
||||
model?: string;
|
||||
effort?: string;
|
||||
limitContext?: boolean;
|
||||
|
|
@ -26,6 +27,7 @@ interface LaunchDialogPrefillInput {
|
|||
|
||||
interface LaunchDialogPrefillResult {
|
||||
providerId: TeamProviderId;
|
||||
providerBackendId?: string;
|
||||
model: string;
|
||||
effort: string;
|
||||
limitContext: boolean;
|
||||
|
|
@ -101,6 +103,10 @@ export function resolveLaunchDialogPrefill({
|
|||
|
||||
return {
|
||||
providerId,
|
||||
providerBackendId:
|
||||
previousLaunchParams?.providerBackendId?.trim() ||
|
||||
savedRequest?.providerBackendId?.trim() ||
|
||||
undefined,
|
||||
model: matchingModel
|
||||
? normalizeExplicitTeamModelForUi(providerId, matchingModel)
|
||||
: getStoredModel(providerId),
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ function areLaunchParamsEquivalent(
|
|||
if (!left || !right) return left === right;
|
||||
return (
|
||||
left.providerId === right.providerId &&
|
||||
left.providerBackendId === right.providerBackendId &&
|
||||
left.model === right.model &&
|
||||
left.effort === right.effort
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1523,6 +1523,7 @@ export interface GlobalTaskDetailState {
|
|||
/** Per-team launch parameters shown in the header badge. */
|
||||
export interface TeamLaunchParams {
|
||||
providerId?: 'anthropic' | 'codex' | 'gemini';
|
||||
providerBackendId?: string;
|
||||
model?: string; // 'opus' | 'sonnet' | 'haiku'
|
||||
effort?: EffortLevel;
|
||||
limitContext?: boolean;
|
||||
|
|
@ -4419,6 +4420,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
const baseModel = extractBaseModel(request.model, request.providerId);
|
||||
const params: TeamLaunchParams = {
|
||||
providerId: request.providerId ?? 'anthropic',
|
||||
providerBackendId: request.providerBackendId,
|
||||
model: baseModel || 'default',
|
||||
effort: request.effort,
|
||||
limitContext: request.limitContext ?? false,
|
||||
|
|
@ -4590,6 +4592,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
const baseModel = extractBaseModel(request.model, request.providerId);
|
||||
const params: TeamLaunchParams = {
|
||||
providerId: request.providerId ?? 'anthropic',
|
||||
providerBackendId: request.providerBackendId,
|
||||
model: baseModel || 'default',
|
||||
effort: request.effort,
|
||||
limitContext: request.limitContext ?? false,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||
import { formatBytes } from '@renderer/utils/formatters';
|
||||
import { formatTeamProviderBackendLabel } from '@renderer/utils/providerBackendIdentity';
|
||||
import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider';
|
||||
|
||||
import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
|
||||
|
|
@ -34,6 +35,10 @@ export function resolveMemberRuntimeSummary(
|
|||
const configuredModel = member.model?.trim() || launchParams?.model?.trim() || '';
|
||||
const configuredEffort = member.effort ?? launchParams?.effort;
|
||||
const runtimeModel = spawnEntry?.runtimeModel?.trim() || runtimeEntry?.runtimeModel?.trim();
|
||||
const backendLabel = formatTeamProviderBackendLabel(
|
||||
configuredProvider,
|
||||
launchParams?.providerBackendId
|
||||
);
|
||||
const memorySuffix =
|
||||
typeof runtimeEntry?.rssBytes === 'number' && runtimeEntry.rssBytes > 0
|
||||
? ` · ${formatBytes(runtimeEntry.rssBytes)}`
|
||||
|
|
@ -41,12 +46,14 @@ export function resolveMemberRuntimeSummary(
|
|||
|
||||
if (runtimeModel && (isMemberLaunchPending(spawnEntry) || configuredModel.length === 0)) {
|
||||
const runtimeProvider = inferTeamProviderIdFromModel(runtimeModel) ?? configuredProvider;
|
||||
return `${formatTeamModelSummary(runtimeProvider, runtimeModel, configuredEffort)}${memorySuffix}`;
|
||||
const summary = formatTeamModelSummary(runtimeProvider, runtimeModel, configuredEffort);
|
||||
return `${summary}${backendLabel ? ` · ${backendLabel}` : ''}${memorySuffix}`;
|
||||
}
|
||||
|
||||
if (isMemberLaunchPending(spawnEntry)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${formatTeamModelSummary(configuredProvider, configuredModel, configuredEffort)}${memorySuffix}`;
|
||||
const summary = formatTeamModelSummary(configuredProvider, configuredModel, configuredEffort);
|
||||
return `${summary}${backendLabel ? ` · ${backendLabel}` : ''}${memorySuffix}`;
|
||||
}
|
||||
|
|
|
|||
53
src/renderer/utils/providerBackendIdentity.ts
Normal file
53
src/renderer/utils/providerBackendIdentity.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { CliProviderStatus, TeamProviderId } from '@shared/types';
|
||||
|
||||
function normalizeOptionalBackendId(value: string | null | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export function resolveEffectiveProviderBackendId(
|
||||
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
|
||||
): string | undefined {
|
||||
return normalizeOptionalBackendId(provider?.resolvedBackendId ?? provider?.selectedBackendId);
|
||||
}
|
||||
|
||||
export function formatTeamProviderBackendLabel(
|
||||
providerId: TeamProviderId | undefined,
|
||||
providerBackendId: string | undefined
|
||||
): string | undefined {
|
||||
const normalizedProviderId = providerId ?? 'anthropic';
|
||||
const normalizedBackendId = normalizeOptionalBackendId(providerBackendId);
|
||||
if (!normalizedBackendId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (normalizedProviderId === 'codex') {
|
||||
switch (normalizedBackendId) {
|
||||
case 'codex-native':
|
||||
return 'Codex native';
|
||||
case 'adapter':
|
||||
return 'Default adapter';
|
||||
case 'api':
|
||||
return 'OpenAI API';
|
||||
case 'auto':
|
||||
return undefined;
|
||||
default:
|
||||
return normalizedBackendId;
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedProviderId === 'gemini') {
|
||||
switch (normalizedBackendId) {
|
||||
case 'cli-sdk':
|
||||
return 'CLI SDK';
|
||||
case 'api':
|
||||
return 'API';
|
||||
case 'auto':
|
||||
return undefined;
|
||||
default:
|
||||
return normalizedBackendId;
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedBackendId;
|
||||
}
|
||||
|
|
@ -784,12 +784,14 @@ export interface TeamViewSnapshot {
|
|||
|
||||
export type EffortLevel = 'low' | 'medium' | 'high';
|
||||
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini';
|
||||
export type TeamProviderBackendId = string;
|
||||
|
||||
export interface TeamLaunchRequest {
|
||||
teamName: string;
|
||||
cwd: string;
|
||||
prompt?: string;
|
||||
providerId?: TeamProviderId;
|
||||
providerBackendId?: TeamProviderBackendId;
|
||||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
/** When true, context window is limited to 200K tokens instead of the default. */
|
||||
|
|
@ -928,6 +930,7 @@ export interface TeamAgentRuntimeSnapshot {
|
|||
teamName: string;
|
||||
updatedAt: string;
|
||||
runId: string | null;
|
||||
providerBackendId?: TeamProviderBackendId;
|
||||
members: Record<string, TeamAgentRuntimeEntry>;
|
||||
}
|
||||
|
||||
|
|
@ -1037,6 +1040,7 @@ export interface TeamCreateRequest {
|
|||
cwd: string;
|
||||
prompt?: string;
|
||||
providerId?: TeamProviderId;
|
||||
providerBackendId?: TeamProviderBackendId;
|
||||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
/** When true, context window is limited to 200K tokens instead of the default. */
|
||||
|
|
@ -1056,6 +1060,7 @@ export interface TeamCreateConfigRequest {
|
|||
color?: string;
|
||||
members: TeamProvisioningMemberInput[];
|
||||
cwd?: string;
|
||||
providerBackendId?: TeamProviderBackendId;
|
||||
}
|
||||
|
||||
export interface TeamCreateResponse {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: '1',
|
||||
CLAUDE_CODE_ENTRY_PROVIDER: 'anthropic',
|
||||
}),
|
||||
'anthropic'
|
||||
'anthropic',
|
||||
undefined
|
||||
);
|
||||
expect(result.connectionIssues).toEqual({
|
||||
anthropic: 'missing key',
|
||||
|
|
@ -163,7 +164,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
HOME: '/Users/electron-home',
|
||||
USERPROFILE: '/Users/electron-home',
|
||||
}),
|
||||
'anthropic'
|
||||
'anthropic',
|
||||
undefined
|
||||
);
|
||||
expect(result.env.HOME).toBe('/Users/electron-home');
|
||||
expect(result.env.USERPROFILE).toBe('/Users/electron-home');
|
||||
|
|
@ -208,7 +210,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
CLAUDE_CODE_CODEX_BACKEND: 'adapter',
|
||||
CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK: '1',
|
||||
}),
|
||||
'codex'
|
||||
'codex',
|
||||
undefined
|
||||
);
|
||||
expect(result.env.CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK).toBe('1');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -442,6 +442,50 @@ describe('TeamProvisioningService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('exposes providerBackendId from the live run request when available', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).configReader = {
|
||||
getConfig: vi.fn(async () => ({
|
||||
members: [{ name: 'team-lead', agentType: 'team-lead' }],
|
||||
})),
|
||||
};
|
||||
(svc as any).teamMetaStore = {
|
||||
getMeta: vi.fn(async () => ({ providerBackendId: 'adapter' })),
|
||||
};
|
||||
(svc as any).aliveRunByTeam.set('runtime-team', 'run-1');
|
||||
(svc as any).runs.set('run-1', {
|
||||
runId: 'run-1',
|
||||
child: { pid: 111 },
|
||||
request: { model: 'gpt-5.4', providerBackendId: 'codex-native' },
|
||||
processKilled: false,
|
||||
cancelRequested: false,
|
||||
spawnContext: null,
|
||||
});
|
||||
vi.mocked(pidusage).mockResolvedValueOnce({
|
||||
'111': createPidusageStat(111, 123_000_000),
|
||||
} as any);
|
||||
|
||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||
|
||||
expect(snapshot.providerBackendId).toBe('codex-native');
|
||||
});
|
||||
|
||||
it('falls back to persisted team meta backend when no live run exists', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).configReader = {
|
||||
getConfig: vi.fn(async () => ({
|
||||
members: [{ name: 'team-lead', agentType: 'team-lead' }],
|
||||
})),
|
||||
};
|
||||
(svc as any).teamMetaStore = {
|
||||
getMeta: vi.fn(async () => ({ providerBackendId: 'codex-native' })),
|
||||
};
|
||||
|
||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||
|
||||
expect(snapshot.providerBackendId).toBe('codex-native');
|
||||
});
|
||||
|
||||
it('falls back to per-pid pidusage reads when batched sampling fails', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).configReader = {
|
||||
|
|
@ -969,6 +1013,63 @@ describe('TeamProvisioningService', () => {
|
|||
expect(launchArgs).toContain(leadSessionId);
|
||||
});
|
||||
|
||||
it('skips --resume when the persisted runtime backend lane changed', async () => {
|
||||
allowConsoleLogs();
|
||||
const teamName = 'resume-backend-change-team';
|
||||
const leadSessionId = 'lead-session-backend-change';
|
||||
writeLaunchConfig(teamName, tempClaudeRoot, leadSessionId, ['alice']);
|
||||
|
||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
||||
vi.mocked(spawnCli).mockImplementation(() => {
|
||||
throw new Error('launch spawn EINVAL');
|
||||
});
|
||||
|
||||
const svc = new TeamProvisioningService(undefined, undefined, undefined, undefined, {
|
||||
writeConfigFile: vi.fn(async () => '/mock/mcp-config-launch.json'),
|
||||
removeConfigFile: vi.fn(async () => {}),
|
||||
} as any);
|
||||
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||
env: { CODEX_API_KEY: 'test' },
|
||||
authSource: 'codex_runtime',
|
||||
}));
|
||||
(svc as any).resolveLaunchExpectedMembers = vi.fn(async () => ({
|
||||
members: [{ name: 'alice' }],
|
||||
source: 'members-meta',
|
||||
warning: undefined,
|
||||
}));
|
||||
(svc as any).normalizeTeamConfigForLaunch = vi.fn(async () => {});
|
||||
(svc as any).assertConfigLeadOnlyForLaunch = vi.fn(async () => {});
|
||||
(svc as any).updateConfigProjectPath = vi.fn(async () => {});
|
||||
(svc as any).restorePrelaunchConfig = vi.fn(async () => {});
|
||||
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||
(svc as any).pathExists = vi.fn(async (targetPath: string) =>
|
||||
targetPath.endsWith(`${leadSessionId}.jsonl`)
|
||||
);
|
||||
(svc as any).teamMetaStore = {
|
||||
getMeta: vi.fn(async () => ({ providerBackendId: 'adapter' })),
|
||||
writeMeta: vi.fn(async () => {}),
|
||||
deleteMeta: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
await expect(
|
||||
svc.launchTeam(
|
||||
{
|
||||
teamName,
|
||||
cwd: tempClaudeRoot,
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
).rejects.toThrow('launch spawn EINVAL');
|
||||
|
||||
const launchArgs = vi.mocked(spawnCli).mock.calls.at(-1)?.[1] as string[];
|
||||
expect(launchArgs).toBeTruthy();
|
||||
expect(launchArgs).not.toContain('--resume');
|
||||
expect(launchArgs).not.toContain(leadSessionId);
|
||||
});
|
||||
|
||||
it('seeds the current lead session id immediately when launch resumes an existing session', async () => {
|
||||
allowConsoleLogs();
|
||||
const teamName = 'resume-seed-session-team';
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
savedRequest: null,
|
||||
previousLaunchParams: {
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.3-codex',
|
||||
effort: 'high',
|
||||
},
|
||||
|
|
@ -116,12 +117,45 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.3-codex',
|
||||
effort: 'high',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a saved request backend lane when no previous launch params exist', () => {
|
||||
const result = resolveLaunchDialogPrefill({
|
||||
members: [],
|
||||
savedRequest: {
|
||||
teamName: 'vector-room-2',
|
||||
cwd: '/Users/test/project',
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
members: [],
|
||||
} as TeamCreateRequest,
|
||||
previousLaunchParams: undefined,
|
||||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
codex: 'gpt-5.4',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not carry a frozen Gemini model into an Anthropic fallback', () => {
|
||||
const members = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2934,6 +2934,49 @@ describe('teamSlice actions', () => {
|
|||
});
|
||||
|
||||
describe('provisioning run scoping', () => {
|
||||
it('persists providerBackendId into createTeam launch params', async () => {
|
||||
const store = createSliceStore();
|
||||
|
||||
await store.getState().createTeam({
|
||||
teamName: 'my-team',
|
||||
cwd: '/tmp/project',
|
||||
members: [],
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
});
|
||||
|
||||
expect(store.getState().launchParamsByTeam['my-team']).toEqual({
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('persists providerBackendId into launchTeam launch params', async () => {
|
||||
const store = createSliceStore();
|
||||
|
||||
await store.getState().launchTeam({
|
||||
teamName: 'my-team',
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
});
|
||||
|
||||
expect(store.getState().launchParamsByTeam['my-team']).toEqual({
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('rolls back optimistic pending run on early createTeam failure', async () => {
|
||||
const store = createSliceStore();
|
||||
hoisted.createTeam.mockRejectedValue(new Error('create failed'));
|
||||
|
|
|
|||
|
|
@ -80,4 +80,22 @@ describe('resolveMemberRuntimeSummary', () => {
|
|||
'5.4 Mini · Medium · 256.0 MB'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the persisted backend lane visible in the runtime summary', () => {
|
||||
const member = createMember({ model: 'gpt-5.4-mini' });
|
||||
|
||||
expect(
|
||||
resolveMemberRuntimeSummary(
|
||||
member,
|
||||
{
|
||||
providerId: 'codex',
|
||||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4-mini',
|
||||
effort: 'medium',
|
||||
limitContext: false,
|
||||
},
|
||||
undefined
|
||||
)
|
||||
).toBe('5.4 Mini · Medium · Codex native');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue