feat(team): warn on large native bootstrap context
This commit is contained in:
parent
eb0dfcc452
commit
da1e5dfc81
3 changed files with 151 additions and 11 deletions
|
|
@ -171,6 +171,7 @@ import {
|
||||||
} from './bootstrap/BootstrapProofValidation';
|
} from './bootstrap/BootstrapProofValidation';
|
||||||
import {
|
import {
|
||||||
buildNativeAppManagedBootstrapSpecs,
|
buildNativeAppManagedBootstrapSpecs,
|
||||||
|
buildNativeAppManagedBootstrapSpecsWithDiagnostics,
|
||||||
type NativeAppManagedBootstrapSpec,
|
type NativeAppManagedBootstrapSpec,
|
||||||
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
|
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
|
||||||
import {
|
import {
|
||||||
|
|
@ -18911,16 +18912,30 @@ export class TeamProvisioningService {
|
||||||
'Building deterministic create bootstrap spec',
|
'Building deterministic create bootstrap spec',
|
||||||
`expectedMembers=${effectiveMemberSpecs.length}`
|
`expectedMembers=${effectiveMemberSpecs.length}`
|
||||||
);
|
);
|
||||||
const nativeAppManagedBootstrapByMember = await buildNativeAppManagedBootstrapSpecs({
|
const nativeBootstrapBuild = await buildNativeAppManagedBootstrapSpecsWithDiagnostics({
|
||||||
teamName: request.teamName,
|
teamName: request.teamName,
|
||||||
cwd: request.cwd,
|
cwd: request.cwd,
|
||||||
members: effectiveMemberSpecs,
|
members: effectiveMemberSpecs,
|
||||||
});
|
});
|
||||||
|
if (nativeBootstrapBuild.diagnostics.warning) {
|
||||||
|
run.progress = {
|
||||||
|
...run.progress,
|
||||||
|
warnings: mergeProvisioningWarnings(
|
||||||
|
run.progress.warnings,
|
||||||
|
nativeBootstrapBuild.diagnostics.warning
|
||||||
|
),
|
||||||
|
};
|
||||||
|
emitProvisioningCheckpoint(
|
||||||
|
run,
|
||||||
|
'Native bootstrap startup context is large',
|
||||||
|
nativeBootstrapBuild.diagnostics.warning
|
||||||
|
);
|
||||||
|
}
|
||||||
const bootstrapSpec = buildDeterministicCreateBootstrapSpec(
|
const bootstrapSpec = buildDeterministicCreateBootstrapSpec(
|
||||||
runId,
|
runId,
|
||||||
request,
|
request,
|
||||||
effectiveMemberSpecs,
|
effectiveMemberSpecs,
|
||||||
nativeAppManagedBootstrapByMember
|
nativeBootstrapBuild.specs
|
||||||
);
|
);
|
||||||
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
|
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
|
||||||
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);
|
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);
|
||||||
|
|
@ -20189,15 +20204,30 @@ export class TeamProvisioningService {
|
||||||
'Building deterministic launch bootstrap spec',
|
'Building deterministic launch bootstrap spec',
|
||||||
`expectedMembers=${effectiveMemberSpecs.length}`
|
`expectedMembers=${effectiveMemberSpecs.length}`
|
||||||
);
|
);
|
||||||
|
const nativeBootstrapBuild = await buildNativeAppManagedBootstrapSpecsWithDiagnostics({
|
||||||
|
teamName: request.teamName,
|
||||||
|
cwd: request.cwd,
|
||||||
|
members: effectiveMemberSpecs,
|
||||||
|
});
|
||||||
|
if (nativeBootstrapBuild.diagnostics.warning) {
|
||||||
|
run.progress = {
|
||||||
|
...run.progress,
|
||||||
|
warnings: mergeProvisioningWarnings(
|
||||||
|
run.progress.warnings,
|
||||||
|
nativeBootstrapBuild.diagnostics.warning
|
||||||
|
),
|
||||||
|
};
|
||||||
|
emitProvisioningCheckpoint(
|
||||||
|
run,
|
||||||
|
'Native bootstrap startup context is large',
|
||||||
|
nativeBootstrapBuild.diagnostics.warning
|
||||||
|
);
|
||||||
|
}
|
||||||
const bootstrapSpec = buildDeterministicLaunchBootstrapSpec(
|
const bootstrapSpec = buildDeterministicLaunchBootstrapSpec(
|
||||||
runId,
|
runId,
|
||||||
request,
|
request,
|
||||||
effectiveMemberSpecs,
|
effectiveMemberSpecs,
|
||||||
await buildNativeAppManagedBootstrapSpecs({
|
nativeBootstrapBuild.specs
|
||||||
teamName: request.teamName,
|
|
||||||
cwd: request.cwd,
|
|
||||||
members: effectiveMemberSpecs,
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
|
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
|
||||||
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);
|
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,23 @@ export interface NativeAppManagedBootstrapSpec {
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NativeAppManagedBootstrapBuildDiagnostics {
|
||||||
|
nativeMemberCount: number;
|
||||||
|
totalContextChars: number;
|
||||||
|
totalContextLimitChars: number;
|
||||||
|
warning: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NativeAppManagedBootstrapBuildResult {
|
||||||
|
specs: Map<string, NativeAppManagedBootstrapSpec>;
|
||||||
|
diagnostics: NativeAppManagedBootstrapBuildDiagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_NATIVE_BOOTSTRAP_BRIEFING_CHARS = 18_000;
|
const MAX_NATIVE_BOOTSTRAP_BRIEFING_CHARS = 18_000;
|
||||||
const MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS = 24_000;
|
const MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS = 24_000;
|
||||||
const MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS = 96_000;
|
export const MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS = 256_000;
|
||||||
|
const NATIVE_BOOTSTRAP_LARGE_ROSTER_MEMBER_COUNT = 7;
|
||||||
|
const NATIVE_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85;
|
||||||
|
|
||||||
export function isNativeAppManagedBootstrapProvider(providerId?: TeamProviderId): boolean {
|
export function isNativeAppManagedBootstrapProvider(providerId?: TeamProviderId): boolean {
|
||||||
return providerId == null || providerId === 'anthropic' || providerId === 'codex';
|
return providerId == null || providerId === 'anthropic' || providerId === 'codex';
|
||||||
|
|
@ -80,6 +94,45 @@ function buildContextText(params: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCompactChars(value: number): string {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `${Math.round(value / 1000)}k chars`;
|
||||||
|
}
|
||||||
|
return `${Math.max(0, Math.round(value))} chars`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNativeBootstrapWarning(params: {
|
||||||
|
nativeMemberCount: number;
|
||||||
|
totalContextChars: number;
|
||||||
|
totalContextLimitChars: number;
|
||||||
|
}): string | null {
|
||||||
|
if (params.nativeMemberCount === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ratio =
|
||||||
|
params.totalContextLimitChars > 0
|
||||||
|
? params.totalContextChars / params.totalContextLimitChars
|
||||||
|
: 0;
|
||||||
|
const isLargeNativeRoster =
|
||||||
|
params.nativeMemberCount >= NATIVE_BOOTSTRAP_LARGE_ROSTER_MEMBER_COUNT;
|
||||||
|
const isNearLimit = ratio >= NATIVE_BOOTSTRAP_NEAR_LIMIT_RATIO;
|
||||||
|
if (!isLargeNativeRoster && !isNearLimit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = `${formatCompactChars(params.totalContextChars)} / ${formatCompactChars(
|
||||||
|
params.totalContextLimitChars
|
||||||
|
)}`;
|
||||||
|
const percent = `${Math.round(ratio * 100)}%`;
|
||||||
|
const rosterHint = `${params.nativeMemberCount} native app-managed member${
|
||||||
|
params.nativeMemberCount === 1 ? '' : 's'
|
||||||
|
}`;
|
||||||
|
return `Large native team startup context: ${usage} (${percent}) across ${rosterHint}. Launch can continue, but if bootstrap confirmation is slow or fails, reduce native member count or use OpenCode for secondary members.`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildLocalNativeMemberBriefing(params: {
|
function buildLocalNativeMemberBriefing(params: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
|
@ -114,6 +167,14 @@ export async function buildNativeAppManagedBootstrapSpecs(params: {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
members: TeamCreateRequest['members'];
|
members: TeamCreateRequest['members'];
|
||||||
}): Promise<Map<string, NativeAppManagedBootstrapSpec>> {
|
}): Promise<Map<string, NativeAppManagedBootstrapSpec>> {
|
||||||
|
return (await buildNativeAppManagedBootstrapSpecsWithDiagnostics(params)).specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildNativeAppManagedBootstrapSpecsWithDiagnostics(params: {
|
||||||
|
teamName: string;
|
||||||
|
cwd: string;
|
||||||
|
members: TeamCreateRequest['members'];
|
||||||
|
}): Promise<NativeAppManagedBootstrapBuildResult> {
|
||||||
const controller = createController({
|
const controller = createController({
|
||||||
teamName: params.teamName,
|
teamName: params.teamName,
|
||||||
claudeDir: getClaudeBasePath(),
|
claudeDir: getClaudeBasePath(),
|
||||||
|
|
@ -121,12 +182,14 @@ export async function buildNativeAppManagedBootstrapSpecs(params: {
|
||||||
});
|
});
|
||||||
const result = new Map<string, NativeAppManagedBootstrapSpec>();
|
const result = new Map<string, NativeAppManagedBootstrapSpec>();
|
||||||
let totalContextChars = 0;
|
let totalContextChars = 0;
|
||||||
|
let nativeMemberCount = 0;
|
||||||
|
|
||||||
for (const member of params.members) {
|
for (const member of params.members) {
|
||||||
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? 'anthropic';
|
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? 'anthropic';
|
||||||
if (!isNativeAppManagedBootstrapProvider(providerId)) {
|
if (!isNativeAppManagedBootstrapProvider(providerId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
nativeMemberCount += 1;
|
||||||
|
|
||||||
let briefing: string;
|
let briefing: string;
|
||||||
try {
|
try {
|
||||||
|
|
@ -182,5 +245,16 @@ export async function buildNativeAppManagedBootstrapSpecs(params: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
const diagnostics: NativeAppManagedBootstrapBuildDiagnostics = {
|
||||||
|
nativeMemberCount,
|
||||||
|
totalContextChars,
|
||||||
|
totalContextLimitChars: MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS,
|
||||||
|
warning: buildNativeBootstrapWarning({
|
||||||
|
nativeMemberCount,
|
||||||
|
totalContextChars,
|
||||||
|
totalContextLimitChars: MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { specs: result, diagnostics };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildNativeAppManagedBootstrapSpecs,
|
buildNativeAppManagedBootstrapSpecs,
|
||||||
|
buildNativeAppManagedBootstrapSpecsWithDiagnostics,
|
||||||
hashNativeBootstrapText,
|
hashNativeBootstrapText,
|
||||||
|
MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS,
|
||||||
} from '../../../../src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder';
|
} from '../../../../src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder';
|
||||||
import { TeamMembersMetaStore } from '../../../../src/main/services/team/TeamMembersMetaStore';
|
import { TeamMembersMetaStore } from '../../../../src/main/services/team/TeamMembersMetaStore';
|
||||||
import { TeamMetaStore } from '../../../../src/main/services/team/TeamMetaStore';
|
import { TeamMetaStore } from '../../../../src/main/services/team/TeamMetaStore';
|
||||||
|
|
@ -99,6 +101,40 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
|
||||||
expect(alice?.contextHash).toBe(hashNativeBootstrapText(alice?.contextText ?? ''));
|
expect(alice?.contextHash).toBe(hashNativeBootstrapText(alice?.contextText ?? ''));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('warns but still builds for large native rosters below the aggregate budget', async () => {
|
||||||
|
await new TeamMetaStore().writeMeta('large-warning-native-team', {
|
||||||
|
cwd: '/tmp/workspace',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
await new TeamMembersMetaStore().writeMembers(
|
||||||
|
'large-warning-native-team',
|
||||||
|
Array.from({ length: 7 }, (_, index) => ({
|
||||||
|
name: `member-${index}`,
|
||||||
|
providerId: 'anthropic' as const,
|
||||||
|
role: 'Developer',
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await buildNativeAppManagedBootstrapSpecsWithDiagnostics({
|
||||||
|
teamName: 'large-warning-native-team',
|
||||||
|
cwd: '/tmp/workspace',
|
||||||
|
members: Array.from({ length: 7 }, (_, index) => ({
|
||||||
|
name: `member-${index}`,
|
||||||
|
providerId: 'anthropic' as const,
|
||||||
|
role: 'Developer',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.specs.size).toBe(7);
|
||||||
|
expect(result.diagnostics.nativeMemberCount).toBe(7);
|
||||||
|
expect(result.diagnostics.totalContextLimitChars).toBe(
|
||||||
|
MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS
|
||||||
|
);
|
||||||
|
expect(result.diagnostics.warning).toMatch(/Large native team startup context/);
|
||||||
|
});
|
||||||
|
|
||||||
it('fails closed when aggregate native context budget is exceeded', async () => {
|
it('fails closed when aggregate native context budget is exceeded', async () => {
|
||||||
const hugeRole = 'x'.repeat(40_000);
|
const hugeRole = 'x'.repeat(40_000);
|
||||||
await new TeamMetaStore().writeMeta('large-native-team', {
|
await new TeamMetaStore().writeMeta('large-native-team', {
|
||||||
|
|
@ -109,7 +145,7 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
|
||||||
});
|
});
|
||||||
await new TeamMembersMetaStore().writeMembers(
|
await new TeamMembersMetaStore().writeMembers(
|
||||||
'large-native-team',
|
'large-native-team',
|
||||||
Array.from({ length: 8 }, (_, index) => ({
|
Array.from({ length: 16 }, (_, index) => ({
|
||||||
name: `member-${index}`,
|
name: `member-${index}`,
|
||||||
providerId: 'anthropic' as const,
|
providerId: 'anthropic' as const,
|
||||||
role: hugeRole,
|
role: hugeRole,
|
||||||
|
|
@ -120,7 +156,7 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
|
||||||
buildNativeAppManagedBootstrapSpecs({
|
buildNativeAppManagedBootstrapSpecs({
|
||||||
teamName: 'large-native-team',
|
teamName: 'large-native-team',
|
||||||
cwd: '/tmp/workspace',
|
cwd: '/tmp/workspace',
|
||||||
members: Array.from({ length: 8 }, (_, index) => ({
|
members: Array.from({ length: 16 }, (_, index) => ({
|
||||||
name: `member-${index}`,
|
name: `member-${index}`,
|
||||||
providerId: 'anthropic' as const,
|
providerId: 'anthropic' as const,
|
||||||
role: hugeRole,
|
role: hugeRole,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue