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'; } 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);

View file

@ -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 };
} }

View file

@ -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,