feat(team): warn on large native bootstrap context

This commit is contained in:
777genius 2026-05-10 15:43:50 +03:00
parent eb0dfcc452
commit da1e5dfc81
3 changed files with 151 additions and 11 deletions

View file

@ -171,6 +171,7 @@ import {
} from './bootstrap/BootstrapProofValidation';
import {
buildNativeAppManagedBootstrapSpecs,
buildNativeAppManagedBootstrapSpecsWithDiagnostics,
type NativeAppManagedBootstrapSpec,
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
import {
@ -18911,16 +18912,30 @@ export class TeamProvisioningService {
'Building deterministic create bootstrap spec',
`expectedMembers=${effectiveMemberSpecs.length}`
);
const nativeAppManagedBootstrapByMember = await buildNativeAppManagedBootstrapSpecs({
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 = buildDeterministicCreateBootstrapSpec(
runId,
request,
effectiveMemberSpecs,
nativeAppManagedBootstrapByMember
nativeBootstrapBuild.specs
);
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);
@ -20189,15 +20204,30 @@ export class TeamProvisioningService {
'Building deterministic launch bootstrap spec',
`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(
runId,
request,
effectiveMemberSpecs,
await buildNativeAppManagedBootstrapSpecs({
teamName: request.teamName,
cwd: request.cwd,
members: effectiveMemberSpecs,
})
nativeBootstrapBuild.specs
);
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);

View file

@ -16,9 +16,23 @@ export interface NativeAppManagedBootstrapSpec {
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_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 {
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: {
teamName: string;
cwd: string;
@ -114,6 +167,14 @@ export async function buildNativeAppManagedBootstrapSpecs(params: {
cwd: string;
members: TeamCreateRequest['members'];
}): 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({
teamName: params.teamName,
claudeDir: getClaudeBasePath(),
@ -121,12 +182,14 @@ export async function buildNativeAppManagedBootstrapSpecs(params: {
});
const result = new Map<string, NativeAppManagedBootstrapSpec>();
let totalContextChars = 0;
let nativeMemberCount = 0;
for (const member of params.members) {
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? 'anthropic';
if (!isNativeAppManagedBootstrapProvider(providerId)) {
continue;
}
nativeMemberCount += 1;
let briefing: string;
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 };
}

View file

@ -6,7 +6,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
buildNativeAppManagedBootstrapSpecs,
buildNativeAppManagedBootstrapSpecsWithDiagnostics,
hashNativeBootstrapText,
MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS,
} from '../../../../src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder';
import { TeamMembersMetaStore } from '../../../../src/main/services/team/TeamMembersMetaStore';
import { TeamMetaStore } from '../../../../src/main/services/team/TeamMetaStore';
@ -99,6 +101,40 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
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 () => {
const hugeRole = 'x'.repeat(40_000);
await new TeamMetaStore().writeMeta('large-native-team', {
@ -109,7 +145,7 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
});
await new TeamMembersMetaStore().writeMembers(
'large-native-team',
Array.from({ length: 8 }, (_, index) => ({
Array.from({ length: 16 }, (_, index) => ({
name: `member-${index}`,
providerId: 'anthropic' as const,
role: hugeRole,
@ -120,7 +156,7 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
buildNativeAppManagedBootstrapSpecs({
teamName: 'large-native-team',
cwd: '/tmp/workspace',
members: Array.from({ length: 8 }, (_, index) => ({
members: Array.from({ length: 16 }, (_, index) => ({
name: `member-${index}`,
providerId: 'anthropic' as const,
role: hugeRole,